keycloak-uncached

Started test tools module

6/19/2014 3:15:19 PM

Details

diff --git a/export-import/export-import-impl/src/main/java/org/keycloak/exportimport/ExportImportConfig.java b/export-import/export-import-impl/src/main/java/org/keycloak/exportimport/ExportImportConfig.java
index d87b55c..f21d4bd 100644
--- a/export-import/export-import-impl/src/main/java/org/keycloak/exportimport/ExportImportConfig.java
+++ b/export-import/export-import-impl/src/main/java/org/keycloak/exportimport/ExportImportConfig.java
@@ -35,6 +35,10 @@ public class ExportImportConfig {
         return System.getProperty(DIR);
     }
 
+    public static String setDir(String dir) {
+        return System.setProperty(DIR, dir);
+    }
+
     public static String getZipFile() {
         return System.getProperty(FILE);
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 644dfbf..af42022 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -104,7 +104,7 @@ public class KeycloakApplication extends Application {
         return uriInfo.getBaseUriBuilder().replacePath(getContextPath()).build();
     }
 
-    protected void loadConfig() {
+    public static void loadConfig() {
         try {
             URL config = null;
 
diff --git a/testsuite/pom.xml b/testsuite/pom.xml
index 766e611..36085e7 100755
--- a/testsuite/pom.xml
+++ b/testsuite/pom.xml
@@ -27,6 +27,7 @@
     <modules>
         <module>integration</module>
         <module>performance</module>
+        <module>tools</module>
     </modules>
 
 </project>
diff --git a/testsuite/tools/pom.xml b/testsuite/tools/pom.xml
new file mode 100755
index 0000000..e6e0f79
--- /dev/null
+++ b/testsuite/tools/pom.xml
@@ -0,0 +1,309 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.0-beta-4-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>keycloak-testsuite-tools</artifactId>
+    <packaging>war</packaging>
+    <name>Keycloak Testsuite Tools</name>
+    <description/>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>net.iharder</groupId>
+            <artifactId>base64</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core-jaxrs</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-services</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-invalidation-cache-model</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-jpa</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-jpa</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-jboss-logging</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-email</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <!-- social -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-social-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-social-github</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-social-google</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-social-twitter</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.twitter4j</groupId>
+            <artifactId>twitter4j-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-social-facebook</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <!-- forms -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-forms-common-themes</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-account-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-account-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-email-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-js-adapter</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <!-- authentication api -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-authentication-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-authentication-model</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-authentication-picketlink</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.picketlink</groupId>
+            <artifactId>picketlink-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.picketlink</groupId>
+            <artifactId>picketlink-idm-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.picketlink</groupId>
+            <artifactId>picketlink-idm-impl</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.picketlink</groupId>
+            <artifactId>picketlink-idm-simple-schema</artifactId>
+        </dependency>
+
+        <!-- timer -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-timer-basic</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- picketlink -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-picketlink-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-picketlink-realm</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.spec.javax.servlet</groupId>
+            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <!-- resteasy -->
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jaxrs</artifactId>
+            <version>${resteasy.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-multipart-provider</artifactId>
+            <version>${resteasy.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>async-http-servlet-3.0</artifactId>
+            <version>${resteasy.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>jaxrs-api</artifactId>
+            <version>${resteasy.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.resteasy</groupId>
+            <artifactId>resteasy-jackson-provider</artifactId>
+            <version>${resteasy.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Mongo dependencies -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-mongo</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-audit-mongo</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mongodb</groupId>
+            <artifactId>mongo-java-driver</artifactId>
+        </dependency>
+
+        <!-- export/import -->
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-export-import-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-export-import-impl</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>de.idyl</groupId>
+            <artifactId>winzipaes</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <finalName>keycloak-tools</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.jboss.as.plugins</groupId>
+                <artifactId>jboss-as-maven-plugin</artifactId>
+                <configuration>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.wildfly.plugins</groupId>
+                <artifactId>wildfly-maven-plugin</artifactId>
+                <configuration>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-deploy-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/testsuite/tools/src/main/java/org/keycloak/test/tools/jobs/CreateUsers.java b/testsuite/tools/src/main/java/org/keycloak/test/tools/jobs/CreateUsers.java
new file mode 100644
index 0000000..2191e6c
--- /dev/null
+++ b/testsuite/tools/src/main/java/org/keycloak/test/tools/jobs/CreateUsers.java
@@ -0,0 +1,81 @@
+package org.keycloak.test.tools.jobs;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.cache.CacheKeycloakSession;
+import org.keycloak.provider.ProviderSession;
+import org.keycloak.provider.ProviderSessionFactory;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.test.tools.PerfTools;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class CreateUsers implements Runnable {
+
+    private PerfTools.Job job;
+    private final ProviderSessionFactory providerSessionFactory;
+    private final String realmName;
+    private int start;
+    private int count;
+    private String prefix;
+    private String[] roles;
+
+    public CreateUsers(PerfTools.Job job, ProviderSessionFactory providerSessionFactory, String realmName, int start, int count, String prefix, String[] roles) {
+        this.job = job;
+        this.providerSessionFactory = providerSessionFactory;
+        this.realmName = realmName;
+        this.start = start;
+        this.count = count;
+        this.prefix = prefix;
+        this.roles = roles;
+    }
+
+    @Override
+    public void run() {
+        job.start();
+
+        ProviderSession providerSession = providerSessionFactory.createSession();
+        try {
+            KeycloakSession session = providerSession.getProvider(CacheKeycloakSession.class);
+
+            session.getTransaction().begin();
+
+            RealmModel realm = new RealmManager(session).getRealmByName(realmName);
+
+            for (int i = start; i < (start + count); i++) {
+                UserModel user = realm.addUser(prefix + "-" + i);
+                user.setEnabled(true);
+                user.setFirstName("First");
+                user.setLastName("Last");
+                user.setEmail(prefix + "-" + i + "@localhost");
+
+                UserCredentialModel password = new UserCredentialModel();
+                password.setType(UserCredentialModel.PASSWORD);
+                password.setValue("password");
+
+                user.updateCredential(password);
+
+                for (String r : roles) {
+                    user.grantRole(realm.getRole(r));
+                }
+
+                job.increment();
+            }
+
+            session.getTransaction().commit();
+        } catch (Throwable t) {
+            StringWriter sw = new StringWriter();
+            t.printStackTrace(new PrintWriter(sw));
+            job.setError(sw.toString());
+        } finally {
+            providerSession.close();
+        }
+    }
+
+}
diff --git a/testsuite/tools/src/main/java/org/keycloak/test/tools/KeycloakTestApplication.java b/testsuite/tools/src/main/java/org/keycloak/test/tools/KeycloakTestApplication.java
new file mode 100644
index 0000000..b8dcf4e
--- /dev/null
+++ b/testsuite/tools/src/main/java/org/keycloak/test/tools/KeycloakTestApplication.java
@@ -0,0 +1,42 @@
+package org.keycloak.test.tools;
+
+import org.jboss.resteasy.core.Dispatcher;
+import org.keycloak.provider.ProviderSessionFactory;
+import org.keycloak.services.resources.KeycloakApplication;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Context;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class KeycloakTestApplication extends Application {
+
+    protected ProviderSessionFactory providerSessionFactory;
+    protected Set<Class<?>> classes = new HashSet<Class<?>>();
+    protected Set<Object> singletons = new HashSet<Object>();
+
+    public KeycloakTestApplication(@Context ServletContext context, @Context Dispatcher dispatcher) {
+        KeycloakApplication.loadConfig();
+
+        this.providerSessionFactory = KeycloakApplication.createProviderSessionFactory();
+
+        context.setAttribute(ProviderSessionFactory.class.getName(), this.providerSessionFactory);
+
+        singletons.add(new PerfTools(providerSessionFactory));
+    }
+
+    @Override
+    public Set<Class<?>> getClasses() {
+        return classes;
+    }
+
+    @Override
+    public Set<Object> getSingletons() {
+        return singletons;
+    }
+
+}
\ No newline at end of file
diff --git a/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java b/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
new file mode 100644
index 0000000..318f7ea
--- /dev/null
+++ b/testsuite/tools/src/main/java/org/keycloak/test/tools/PerfTools.java
@@ -0,0 +1,178 @@
+package org.keycloak.test.tools;
+
+import org.keycloak.exportimport.ExportImportConfig;
+import org.keycloak.exportimport.ExportImportProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderSession;
+import org.keycloak.provider.ProviderSessionFactory;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.test.tools.jobs.CreateUsers;
+import org.keycloak.util.ProviderLoader;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+@Path("perf")
+public class PerfTools {
+
+    private ExecutorService executor = Executors.newFixedThreadPool(20);
+
+    private final ProviderSessionFactory providerSessionFactory;
+
+    @Context
+    private KeycloakSession session;
+
+    private List<Job> jobs = new LinkedList<Job>();
+
+    public PerfTools(ProviderSessionFactory providerSessionFactory) {
+        this.providerSessionFactory = providerSessionFactory;
+    }
+
+    @GET
+    @Path("jobs")
+    @Produces("application/json")
+    public List<Job> jobs() {
+        return jobs;
+    }
+
+    @GET
+    @Path("delete-jobs")
+    public void deleteJobs() {
+        Iterator<Job> itr = jobs.iterator();
+        while(itr.hasNext()) {
+            Job j = itr.next();
+            if (j.getError() != null || j.getCount() == j.getTotal()) {
+                itr.remove();
+            }
+        }
+    }
+
+    @GET
+    @Path("{realm}/create-users")
+    public Response createUsers(@PathParam("realm") String realmName, @QueryParam("count") Integer count, @QueryParam("batch") Integer batch, @QueryParam("start") Integer start, @QueryParam("prefix") String prefix, @QueryParam("roles") String roles) throws InterruptedException {
+        if (count == null) {
+            count = 1;
+        }
+        if (batch == null) {
+            batch = 1000;
+        }
+        if (start == null) {
+            start = 0;
+        }
+        if (prefix == null) {
+            prefix = String.valueOf(System.currentTimeMillis());
+        }
+
+        String[] rolesArray = roles != null ? roles.split(",") : new String[0];
+
+        Job job = new Job("Create users " + prefix + "-" + start + " to " + prefix + "-" + (start + count), count);
+        jobs.add(job);
+
+        for (int s = start; s < (start + count); s += batch) {
+            int c = s + batch <= (start + count) ? batch : (start + count) - s;
+            executor.submit(new CreateUsers(job, providerSessionFactory, realmName, s, c, prefix, rolesArray));
+        }
+
+        return Response.noContent().build();
+    }
+
+    @GET
+    @Path("{realm}/delete-users")
+    public void deleteUsers(@PathParam("realm") String realmName) {
+        RealmModel realm = session.getRealmByName(realmName);
+        for (UserModel user : realm.getUsers()) {
+            realm.removeUser(user.getLoginName());
+        }
+    }
+
+    @GET
+    @Path("export")
+    public void export(@QueryParam("dir") String dir) {
+        ExportImportConfig.setAction("export");
+        ExportImportConfig.setProvider("dir");
+        ExportImportConfig.setDir(dir);
+
+        Iterator<ExportImportProvider> providers = ProviderLoader.load(ExportImportProvider.class).iterator();
+
+        if (providers.hasNext()) {
+            ExportImportProvider exportImport = providers.next();
+            exportImport.checkExportImport(providerSessionFactory);
+        } else {
+            throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    public class Job {
+        private final String description;
+        private final int total;
+        private AtomicInteger count = new AtomicInteger();
+        private String error;
+        private AtomicLong started = new AtomicLong();
+        private AtomicLong completed = new AtomicLong();
+
+        public Job(String description, int total) {
+            this.description = description;
+            this.total = total;
+        }
+
+        public Long getStarted() {
+            long s = started.get();
+            return s != 0 ? s : null;
+        }
+
+        public Long getCompleted() {
+            long c = completed.get();
+            return c != 0 ? c : null;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public int getTotal() {
+            return total;
+        }
+
+        public int getCount() {
+            return count.get();
+        }
+
+        public void start() {
+            started.compareAndSet(0, System.currentTimeMillis());
+        }
+
+        public void increment() {
+            if (count.incrementAndGet() == total) {
+                completed.set(System.currentTimeMillis());
+            }
+        }
+
+        public String getError() {
+            return error;
+        }
+
+        public void setError(String error) {
+            this.error = error;
+        }
+    }
+
+}
diff --git a/testsuite/tools/src/main/resources/META-INF/keycloak-server.json b/testsuite/tools/src/main/resources/META-INF/keycloak-server.json
new file mode 100755
index 0000000..fb726ca
--- /dev/null
+++ b/testsuite/tools/src/main/resources/META-INF/keycloak-server.json
@@ -0,0 +1,49 @@
+{
+    "admin": {
+        "realm": "master"
+    },
+
+    "audit": {
+        "provider": "jpa",
+        "jpa": {
+            "exclude-events": [ "REFRESH_TOKEN" ]
+        }
+    },
+
+    "model": {
+        "provider": "jpa"
+    },
+
+    "modelCache": {
+        "provider": "${keycloak.model.cache.provider:none}"
+    },
+
+    "timer": {
+        "provider": "basic"
+    },
+
+    "theme": {
+        "default": "keycloak",
+        "staticMaxAge": 2592000,
+        "cacheTemplates": true,
+        "folder": {
+          "dir": "${jboss.server.config.dir}/themes"
+        }
+    },
+
+    "login-forms": {
+        "provider": "freemarker"
+    },
+
+    "account": {
+        "provider": "freemarker"
+    },
+
+    "email": {
+        "provider": "freemarker"
+    },
+
+    "scheduled": {
+        "interval": 900
+    }
+}
\ No newline at end of file
diff --git a/testsuite/tools/src/main/resources/META-INF/persistence.xml b/testsuite/tools/src/main/resources/META-INF/persistence.xml
new file mode 100755
index 0000000..ee721fe
--- /dev/null
+++ b/testsuite/tools/src/main/resources/META-INF/persistence.xml
@@ -0,0 +1,41 @@
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
+    version="1.0">
+    <persistence-unit name="jpa-keycloak-identity-store" transaction-type="RESOURCE_LOCAL">
+        <jta-data-source>java:jboss/datasources/KeycloakDS</jta-data-source>
+        <class>org.keycloak.models.jpa.entities.ApplicationEntity</class>
+        <class>org.keycloak.models.jpa.entities.CredentialEntity</class>
+        <class>org.keycloak.models.jpa.entities.OAuthClientEntity</class>
+        <class>org.keycloak.models.jpa.entities.RealmEntity</class>
+        <class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class>
+        <class>org.keycloak.models.jpa.entities.AuthenticationProviderEntity</class>
+        <class>org.keycloak.models.jpa.entities.RoleEntity</class>
+        <class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
+        <class>org.keycloak.models.jpa.entities.AuthenticationLinkEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserSessionEntity</class>
+        <class>org.keycloak.models.jpa.entities.ClientUserSessionAssociationEntity</class>
+        <class>org.keycloak.models.jpa.entities.UsernameLoginFailureEntity</class>
+        <class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
+        <class>org.keycloak.models.jpa.entities.ScopeMappingEntity</class>
+
+        <exclude-unlisted-classes>true</exclude-unlisted-classes>
+
+        <properties>
+            <property name="hibernate.hbm2ddl.auto" value="update" />
+        </properties>
+    </persistence-unit>
+	
+    <persistence-unit name="jpa-keycloak-audit-store" transaction-type="RESOURCE_LOCAL">
+        <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
+        <class>org.keycloak.audit.jpa.EventEntity</class>
+
+        <exclude-unlisted-classes>true</exclude-unlisted-classes>
+
+        <properties>
+            <property name="hibernate.hbm2ddl.auto" value="update" />
+        </properties>
+    </persistence-unit>
+	
+</persistence>
diff --git a/testsuite/tools/src/main/webapp/index.html b/testsuite/tools/src/main/webapp/index.html
new file mode 100644
index 0000000..19521ec
--- /dev/null
+++ b/testsuite/tools/src/main/webapp/index.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <title>Keycloak Testsuite Tools</title>
+
+    <link rel="stylesheet" href="/auth/admin/master/console/css/styles.css">
+
+    <script src="/auth/admin/master/console/lib/angular/angular.js"></script>
+    <script src="/auth/admin/master/console/lib/angular/angular-resource.js"></script>
+    <script src="/auth/admin/master/console/lib/angular/angular-route.js"></script>
+
+    <script src="js/app.js"></script>
+</head>
+
+<body data-ng-app="keycloak-tools">
+
+<header class="navbar navbar-default navbar-pf navbar-main header">
+
+    <ul class="nav navbar-nav navbar-primary persistent-secondary ng-scope">
+        <li><a href="#/">Home</a></li>
+        <li><a href="#/perf">Perf</a></li>
+    </ul>
+</header>
+
+
+
+<div style="background-color: #fff; margin: 2em; padding: 2em;" data-ng-view id="view"></div>
+
+</body>
+
diff --git a/testsuite/tools/src/main/webapp/js/app.js b/testsuite/tools/src/main/webapp/js/app.js
new file mode 100644
index 0000000..76e4f96
--- /dev/null
+++ b/testsuite/tools/src/main/webapp/js/app.js
@@ -0,0 +1,49 @@
+var module = angular.module('keycloak-tools', [ 'ngRoute', 'ngResource' ]);
+
+module.config([ '$routeProvider', function ($routeProvider) {
+
+    $routeProvider
+        .when('/perf', {
+            templateUrl: 'pages/perf.html',
+            controller: 'PerfCtrl'
+        })
+        .otherwise({
+            templateUrl: 'pages/home.html'
+        });
+}]);
+
+module.filter('reverse', function() {
+    return function(items) {
+        return items.slice().reverse();
+    };
+});
+
+module.controller('PerfCtrl', function ($scope, $resource) {
+
+    $scope.createUsersData = {
+        realm: 'test',
+        count: 100,
+        start: 0,
+        batch: 25
+    }
+
+    $scope.loadJobs = function() {
+        $scope.jobs = $resource('/keycloak-tools/perf/jobs').query();
+    }
+
+    $scope.clearJobs = function() {
+        $scope.jobs = $resource('/keycloak-tools/perf/delete-jobs').query({}, function() {
+            $scope.loadJobs();
+        });
+    }
+
+    $scope.createUsers = function() {
+        console.debug($scope.createUsersData);
+        $resource('/keycloak-tools/perf/:realm/create-users').get($scope.createUsersData, function() {
+            $scope.loadJobs();
+        });
+    }
+
+    $scope.loadJobs();
+
+});
diff --git a/testsuite/tools/src/main/webapp/pages/home.html b/testsuite/tools/src/main/webapp/pages/home.html
new file mode 100644
index 0000000..357dd48
--- /dev/null
+++ b/testsuite/tools/src/main/webapp/pages/home.html
@@ -0,0 +1 @@
+Home
\ No newline at end of file
diff --git a/testsuite/tools/src/main/webapp/pages/perf.html b/testsuite/tools/src/main/webapp/pages/perf.html
new file mode 100644
index 0000000..84a3023
--- /dev/null
+++ b/testsuite/tools/src/main/webapp/pages/perf.html
@@ -0,0 +1,67 @@
+<h1>Perf</h1>
+
+<fieldset>
+    <legend><span class="text">Jobs</span></legend>
+    <button data-ng-click="loadJobs()" class="btn btn-default">Refresh</button>
+    <button data-ng-click="clearJobs()" class="btn btn-default">Clear completed</button>
+
+    <table class="table table-striped table-bordered">
+        <thead>
+        <tr>
+            <th>Description</th>
+            <th>Error</th>
+            <th>Count</th>
+            <th>Total</th>
+            <th>Started</th>
+            <th>Completed</th>
+        </tr>
+        </thead>
+        <tr data-ng-repeat="j in jobs|reverse">
+            <td>{{j.description}}</td>
+            <td>{{j.error}}</td>
+            <td>{{j.count}}</td>
+            <td>{{j.total}}</td>
+            <td>{{j.started|date:'medium'}}</td>
+            <td>{{j.completed|date:'medium'}}</td>
+        </tr>
+    </table>
+</fieldset>
+
+<fieldset>
+    <legend><span class="text">Create users</span></legend>
+
+    <div class="alert alert-info"><span class="pficon pficon-info"></span> Create users with username <code>&lt;prefix&gt;-&lt;start&gt;</code> to <code>&lt;prefix&gt;-&lt;start + count&gt;</code> and optionally add role mappings for the specified realm roles.</div>
+
+    <form class="form-horizontal">
+        <div class="form-group">
+            <label class="control-label col-sm-1">Realm</label>
+            <input data-ng-model="createUsersData.realm" value="test">
+        </div>
+        <div class="form-group">
+            <label class="control-label col-sm-1">Count</label>
+            <input data-ng-model="createUsersData.count" type="number" min="1" max="100000">
+            <span>(number of users to create)</span>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-sm-1">Batch</label>
+            <input data-ng-model="createUsersData.batch" type="number" min="1" max="100">
+            <span>(number of users to create in one transaction)</span>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-sm-1">Start</label>
+            <input data-ng-model="createUsersData.start" type="number" min="0">
+        </div>
+        <div class="form-group">
+            <label class="control-label col-sm-1">Prefix</label>
+            <input data-ng-model="createUsersData.prefix" type="number" min="0">
+            <span>(optional, by default currentTimeMillis is used)</span>
+        </div>
+        <div class="form-group">
+            <label class="control-label col-sm-1">Roles</label>
+            <input data-ng-model="createUsersData.roles" type="number" min="0">
+            <span>(comma separated list of realm roles)</span>
+        </div>
+
+        <button data-ng-click="createUsers()" class="btn btn-default">Create</button>
+    </form>
+</fieldset>
\ No newline at end of file
diff --git a/testsuite/tools/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/testsuite/tools/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
new file mode 100755
index 0000000..0925383
--- /dev/null
+++ b/testsuite/tools/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
@@ -0,0 +1,20 @@
+<jboss-deployment-structure>
+    <deployment>
+        <dependencies>
+            <module name="org.apache.httpcomponents"/>
+            <module name="org.bouncycastle"/>
+            <module name="org.jboss.resteasy.resteasy-jackson-provider" services="import"/>
+            <module name="org.codehaus.jackson.jackson-core-asl"/>
+            <module name="org.codehaus.jackson.jackson-mapper-asl"/>
+        </dependencies>
+        <exclusions>
+            <module name="org.jboss.resteasy.resteasy-jackson2-provider"/>
+
+            <!-- Exclude keycloak modules -->
+            <module name="org.keycloak.keycloak-core" />
+            <module name="org.keycloak.keycloak-adapter-core" />
+            <module name="org.keycloak.keycloak-undertow-adapter" />
+            <module name="org.keycloak.keycloak-as7-adapter" />
+        </exclusions>
+    </deployment>
+</jboss-deployment-structure>
\ No newline at end of file
diff --git a/testsuite/tools/src/main/webapp/WEB-INF/web.xml b/testsuite/tools/src/main/webapp/WEB-INF/web.xml
new file mode 100755
index 0000000..aea2f9c
--- /dev/null
+++ b/testsuite/tools/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+      version="3.0">
+
+	<module-name>keycloak-tools</module-name>
+
+    <servlet>
+        <servlet-name>Keycloak REST Interface</servlet-name>
+        <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher</servlet-class>
+        <init-param>
+            <param-name>javax.ws.rs.Application</param-name>
+            <param-value>org.keycloak.test.tools.KeycloakTestApplication</param-value>
+        </init-param>
+        <init-param>
+            <param-name>resteasy.servlet.mapping.prefix</param-name>
+            <param-value>/</param-value>
+        </init-param>
+        <load-on-startup>1</load-on-startup>
+        <async-supported>true</async-supported>
+    </servlet>
+    
+    <welcome-file-list>
+        <welcome-file>index.html</welcome-file>
+    </welcome-file-list>
+
+    <listener>
+        <listener-class>org.keycloak.services.listeners.KeycloakSessionDestroyListener</listener-class>
+    </listener>
+
+    <filter>
+        <filter-name>Keycloak Client Connection Filter</filter-name>
+        <filter-class>org.keycloak.services.filters.ClientConnectionFilter</filter-class>
+    </filter>
+
+    <filter>
+        <filter-name>Keycloak Session Management</filter-name>
+        <filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class>
+    </filter>
+
+    <filter-mapping>
+        <filter-name>Keycloak Session Management</filter-name>
+        <url-pattern>/perf/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+        <filter-name>Keycloak Client Connection Filter</filter-name>
+        <url-pattern>/perf/*</url-pattern>
+    </filter-mapping>
+
+    <servlet-mapping>
+        <servlet-name>Keycloak REST Interface</servlet-name>
+        <url-pattern>/perf/*</url-pattern>
+    </servlet-mapping>
+
+    <!--
+
+    <security-constraint>
+        <web-resource-collection>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <user-data-constraint>
+            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
+        </user-data-constraint>
+    </security-constraint>
+    -->
+
+
+</web-app>