keycloak-uncached
Changes
examples/as7-eap-demo/server/pom.xml 9(+8 -1)
forms/pom.xml 6(+0 -6)
model/mongo/pom.xml 77(+77 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractAttributedNoSQLObject.java 37(+37 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectConverter.java 102(+102 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/EnumToStringConverter.java 26(+26 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/StringToEnumConverter.java 32(+32 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java 284(+284 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoDBSessionFactory.java 70(+70 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLTransaction.java 39(+39 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java 54(+54 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java 862(+862 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/PasswordCredentialHandler.java 154(+154 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java 138(+138 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java 66(+66 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java 66(+66 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java 90(+90 -0)
model/picketlink/src/main/java/org/keycloak/models/picketlink/PicketlinkKeycloakSession.java 8(+2 -6)
model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java 16(+16 -0)
model/pom.xml 1(+1 -0)
pom.xml 85(+80 -5)
services/pom.xml 23(+21 -2)
testsuite/integration/pom.xml 253(+253 -0)
testsuite/integration/README.md 31(+17 -14)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java 0(+0 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java 0(+0 -0)
testsuite/integration/src/test/resources/META-INF/services/org.keycloak.social.SocialProvider 0(+0 -0)
testsuite/performance/pom.xml 240(+240 -0)
testsuite/performance/src/test/java/org/keycloak/testsuite/performance/BaseJMeterPerformanceTest.java 140(+140 -0)
testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateRealmsWorker.java 101(+101 -0)
testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateUsersWorker.java 120(+120 -0)
testsuite/performance/src/test/java/org/keycloak/testsuite/performance/PerfTestUtils.java 46(+46 -0)
testsuite/performance/src/test/java/org/keycloak/testsuite/performance/ReadUsersWorker.java 127(+127 -0)
testsuite/performance/src/test/java/org/keycloak/testsuite/performance/RemoveUsersWorker.java 72(+72 -0)
testsuite/pom.xml 217(+7 -210)
Details
examples/as7-eap-demo/server/pom.xml 9(+8 -1)
diff --git a/examples/as7-eap-demo/server/pom.xml b/examples/as7-eap-demo/server/pom.xml
index 7027dc5..3282368 100755
--- a/examples/as7-eap-demo/server/pom.xml
+++ b/examples/as7-eap-demo/server/pom.xml
@@ -118,7 +118,14 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
- <version>1.3.161</version>
+ </dependency>
+ <dependency>
+ <groupId>org.mongodb</groupId>
+ <artifactId>mongo-java-driver</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>de.flapdoodle.embed</groupId>
+ <artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
diff --git a/examples/as7-eap-demo/server/src/main/webapp/WEB-INF/web.xml b/examples/as7-eap-demo/server/src/main/webapp/WEB-INF/web.xml
index 6829b0e..fafd744 100755
--- a/examples/as7-eap-demo/server/src/main/webapp/WEB-INF/web.xml
+++ b/examples/as7-eap-demo/server/src/main/webapp/WEB-INF/web.xml
@@ -21,6 +21,10 @@
<async-supported>true</async-supported>
</servlet>
+ <listener>
+ <listener-class>org.keycloak.services.listeners.MongoRunnerListener</listener-class>
+ </listener>
+
<filter>
<filter-name>Keycloak Session Management</filter-name>
<filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class>
forms/pom.xml 6(+0 -6)
diff --git a/forms/pom.xml b/forms/pom.xml
index 6d0dec8..0af1892 100755
--- a/forms/pom.xml
+++ b/forms/pom.xml
@@ -46,12 +46,6 @@
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
- <version>1.0.2.Final</version>
- </dependency>
- <dependency>
- <groupId>javax.faces</groupId>
- <artifactId>jsf-api</artifactId>
- <version>2.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakSessionUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakSessionUtils.java
new file mode 100644
index 0000000..9119282
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakSessionUtils.java
@@ -0,0 +1,15 @@
+package org.keycloak.models.utils;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class KeycloakSessionUtils {
+
+ private static AtomicLong counter = new AtomicLong(1);
+
+ public static String generateId() {
+ return counter.getAndIncrement() + "-" + System.currentTimeMillis();
+ }
+}
model/mongo/pom.xml 77(+77 -0)
diff --git a/model/mongo/pom.xml b/model/mongo/pom.xml
new file mode 100644
index 0000000..537112a
--- /dev/null
+++ b/model/mongo/pom.xml
@@ -0,0 +1,77 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-alpha-1</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-model-mongo</artifactId>
+ <name>Keycloak Model Mongo</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk16</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-common</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-idm-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mongodb</groupId>
+ <artifactId>mongo-java-driver</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>de.flapdoodle.embed</groupId>
+ <artifactId>de.flapdoodle.embed.mongo</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
\ No newline at end of file
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractAttributedNoSQLObject.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractAttributedNoSQLObject.java
new file mode 100644
index 0000000..81546ba
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractAttributedNoSQLObject.java
@@ -0,0 +1,37 @@
+package org.keycloak.models.mongo.api;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractAttributedNoSQLObject extends AbstractNoSQLObject implements AttributedNoSQLObject {
+
+ // Simple hashMap for now (no thread-safe)
+ private Map<String, String> attributes = new HashMap<String, String>();
+
+ @Override
+ public void setAttribute(String name, String value) {
+ attributes.put(name, value);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ // attributes.remove(name);
+
+ // ensure that particular attribute has null value, so it will be deleted in DB. TODO: needs to be improved
+ attributes.put(name, null);
+ }
+
+ @Override
+ public String getAttribute(String name) {
+ return attributes.get(name);
+ }
+
+ @Override
+ public Map<String, String> getAttributes() {
+ return Collections.unmodifiableMap(attributes);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractNoSQLObject.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractNoSQLObject.java
new file mode 100644
index 0000000..837e5e4
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractNoSQLObject.java
@@ -0,0 +1,12 @@
+package org.keycloak.models.mongo.api;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractNoSQLObject implements NoSQLObject {
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ // Empty by default
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/AttributedNoSQLObject.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AttributedNoSQLObject.java
new file mode 100644
index 0000000..45accd1
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AttributedNoSQLObject.java
@@ -0,0 +1,17 @@
+package org.keycloak.models.mongo.api;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface AttributedNoSQLObject extends NoSQLObject {
+
+ void setAttribute(String name, String value);
+
+ void removeAttribute(String name);
+
+ String getAttribute(String name);
+
+ Map<String, String> getAttributes();
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQL.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQL.java
new file mode 100644
index 0000000..3bc62a5
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQL.java
@@ -0,0 +1,37 @@
+package org.keycloak.models.mongo.api;
+
+import java.util.List;
+
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.api.query.NoSQLQueryBuilder;
+import org.picketlink.common.properties.Property;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface NoSQL {
+
+ /**
+ * Insert object if it's oid is null. Otherwise update
+ */
+ void saveObject(NoSQLObject object);
+
+ <T extends NoSQLObject> T loadObject(Class<T> type, String oid);
+
+ <T extends NoSQLObject> T loadSingleObject(Class<T> type, NoSQLQuery query);
+
+ <T extends NoSQLObject> List<T> loadObjects(Class<T> type, NoSQLQuery query);
+
+ // Object must have filled oid
+ void removeObject(NoSQLObject object);
+
+ void removeObject(Class<? extends NoSQLObject> type, String oid);
+
+ void removeObjects(Class<? extends NoSQLObject> type, NoSQLQuery query);
+
+ NoSQLQueryBuilder createQueryBuilder();
+
+ <S> void pushItemToList(NoSQLObject object, String listPropertyName, S itemToPush);
+
+ <S> void pullItemFromList(NoSQLObject object, String listPropertyName, S itemToPull);
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLCollection.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLCollection.java
new file mode 100644
index 0000000..80b6332
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLCollection.java
@@ -0,0 +1,21 @@
+package org.keycloak.models.mongo.api;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Target({TYPE})
+@Documented
+@Retention(RUNTIME)
+@Inherited
+public @interface NoSQLCollection {
+
+ String collectionName();
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLField.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLField.java
new file mode 100644
index 0000000..3af69a7
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLField.java
@@ -0,0 +1,20 @@
+package org.keycloak.models.mongo.api;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Target({METHOD, FIELD})
+@Documented
+@Retention(RUNTIME)
+public @interface NoSQLField {
+
+ // TODO: fieldName add lazy loading?
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLId.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLId.java
new file mode 100644
index 0000000..06ed01e
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLId.java
@@ -0,0 +1,18 @@
+package org.keycloak.models.mongo.api;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@Target({METHOD, FIELD})
+@Documented
+@Retention(RUNTIME)
+public @interface NoSQLId {
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLObject.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLObject.java
new file mode 100644
index 0000000..0242243
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLObject.java
@@ -0,0 +1,16 @@
+package org.keycloak.models.mongo.api;
+
+/**
+ * Base interface for object, which is persisted in NoSQL database
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface NoSQLObject {
+
+ /**
+ * Lifecycle callback, which is called after removal of this object from NoSQL database.
+ * It may be useful for triggering removal of wired objects.
+ */
+ void afterRemove(NoSQL noSQL);
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQuery.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQuery.java
new file mode 100644
index 0000000..29cc0f3
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQuery.java
@@ -0,0 +1,26 @@
+package org.keycloak.models.mongo.api.query;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class NoSQLQuery {
+
+ private final Map<String, Object> queryAttributes;
+
+ NoSQLQuery(Map<String, Object> queryAttributes) {
+ this.queryAttributes = queryAttributes;
+ };
+
+ public Map<String, Object> getQueryAttributes() {
+ return Collections.unmodifiableMap(queryAttributes);
+ }
+
+ @Override
+ public String toString() {
+ return "NoSQLQuery [" + queryAttributes + "]";
+ }
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQueryBuilder.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQueryBuilder.java
new file mode 100644
index 0000000..dcdb575
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQueryBuilder.java
@@ -0,0 +1,31 @@
+package org.keycloak.models.mongo.api.query;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class NoSQLQueryBuilder {
+
+ private Map<String, Object> queryAttributes = new HashMap<String, Object>();
+
+ protected NoSQLQueryBuilder() {};
+
+ public NoSQLQuery build() {
+ return new NoSQLQuery(queryAttributes);
+ }
+
+ public NoSQLQueryBuilder andCondition(String name, Object value) {
+ this.put(name, value);
+ return this;
+ }
+
+ public abstract NoSQLQueryBuilder inCondition(String name, List<?> values);
+
+ protected void put(String name, Object value) {
+ queryAttributes.put(name, value);
+ }
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/Converter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/Converter.java
new file mode 100644
index 0000000..a6b6c86
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/Converter.java
@@ -0,0 +1,16 @@
+package org.keycloak.models.mongo.api.types;
+
+/**
+ * SPI object to convert object from application type to database type and vice versa. Shouldn't be directly used by application.
+ * Various converters should be registered in TypeConverter, which is main entry point to be used by application
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface Converter<T, S> {
+
+ S convertObject(T objectToConvert);
+
+ Class<? extends T> getConverterObjectType();
+
+ Class<S> getExpectedReturnType();
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/TypeConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/TypeConverter.java
new file mode 100644
index 0000000..a7c12c0
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/TypeConverter.java
@@ -0,0 +1,114 @@
+package org.keycloak.models.mongo.api.types;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.picketlink.common.reflection.Reflections;
+
+/**
+ * Registry of converters, which allow to convert application object to database objects. TypeConverter is main entry point to be used by application.
+ * Application can create instance of TypeConverter and then register required Converter objects.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TypeConverter {
+
+ // TODO: Thread-safety support (maybe...)
+ // Converters of Application objects to DB objects
+ private Map<Class<?>, Converter<?, ?>> appObjectConverters = new HashMap<Class<?>, Converter<?, ?>>();
+
+ // Converters of DB objects to Application objects
+ private Map<Class<?>, Map<Class<?>, Converter<?, ?>>> dbObjectConverters = new HashMap<Class<?>, Map<Class<?>, Converter<?,?>>>();
+
+
+ /**
+ * Add converter for converting application objects to DB objects
+ *
+ * @param converter
+ */
+ public void addAppObjectConverter(Converter<?, ?> converter) {
+ appObjectConverters.put(converter.getConverterObjectType(), converter);
+ }
+
+
+ /**
+ * Add converter for converting DB objects to application objects
+ *
+ * @param converter
+ */
+ public void addDBObjectConverter(Converter<?, ?> converter) {
+ Class<?> dbObjectType = converter.getConverterObjectType();
+ Class<?> appObjectType = converter.getExpectedReturnType();
+ Map<Class<?>, Converter<?, ?>> appObjects = dbObjectConverters.get(dbObjectType);
+ if (appObjects == null) {
+ appObjects = new HashMap<Class<?>, Converter<?, ?>>();
+ dbObjectConverters.put(dbObjectType, appObjects);
+ }
+ appObjects.put(appObjectType, converter);
+ }
+
+
+ public <S> S convertDBObjectToApplicationObject(Object dbObject, Class<S> expectedApplicationObjectType) {
+ Class<?> dbObjectType = dbObject.getClass();
+ Converter<Object, S> converter;
+
+ Map<Class<?>, Converter<?, ?>> appObjects = dbObjectConverters.get(dbObjectType);
+ if (appObjects == null) {
+ throw new IllegalArgumentException("Not found any converters for type " + dbObjectType);
+ } else {
+ if (appObjects.size() == 1) {
+ converter = (Converter<Object, S>)appObjects.values().iterator().next();
+ } else {
+ // Try to find converter for requested application type
+ converter = (Converter<Object, S>)getAppConverterForType(expectedApplicationObjectType, appObjects);
+ }
+ }
+
+ if (converter == null) {
+ throw new IllegalArgumentException("Can't found converter for type " + dbObjectType + " and expectedApplicationType " + expectedApplicationObjectType);
+ }
+ /*if (!expectedApplicationObjectType.isAssignableFrom(converter.getExpectedReturnType())) {
+ throw new IllegalArgumentException("Converter " + converter + " has return type " + converter.getExpectedReturnType() +
+ " but we need type " + expectedApplicationObjectType);
+ } */
+
+ return converter.convertObject(dbObject);
+ }
+
+
+ public <S> S convertApplicationObjectToDBObject(Object applicationObject, Class<S> expectedDBObjectType) {
+ Class<?> appObjectType = applicationObject.getClass();
+ Converter<Object, S> converter = (Converter<Object, S>)getAppConverterForType(appObjectType, appObjectConverters);
+ if (converter == null) {
+ throw new IllegalArgumentException("Can't found converter for type " + appObjectType + " in registered appObjectConverters");
+ }
+ if (!expectedDBObjectType.isAssignableFrom(converter.getExpectedReturnType())) {
+ throw new IllegalArgumentException("Converter " + converter + " has return type " + converter.getExpectedReturnType() +
+ " but we need type " + expectedDBObjectType);
+ }
+ return converter.convertObject(applicationObject);
+ }
+
+ // Try to find converter for given type or all it's supertypes
+ private static Converter<Object, ?> getAppConverterForType(Class<?> appObjectType, Map<Class<?>, Converter<?, ?>> appObjectConverters) {
+ Converter<Object, ?> converter = (Converter<Object, ?>)appObjectConverters.get(appObjectType);
+ if (converter != null) {
+ return converter;
+ } else {
+ Class<?>[] interfaces = appObjectType.getInterfaces();
+ for (Class<?> interface1 : interfaces) {
+ converter = getAppConverterForType(interface1, appObjectConverters);
+ if (converter != null) {
+ return converter;
+ }
+ }
+
+ Class<?> superType = appObjectType.getSuperclass();
+ if (superType != null) {
+ return getAppConverterForType(superType, appObjectConverters);
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBImpl.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBImpl.java
new file mode 100644
index 0000000..09ca78e
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBImpl.java
@@ -0,0 +1,324 @@
+package org.keycloak.models.mongo.impl;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DB;
+import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
+import com.mongodb.DBObject;
+import org.bson.types.ObjectId;
+import org.jboss.logging.Logger;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.api.query.NoSQLQueryBuilder;
+import org.keycloak.models.mongo.api.types.Converter;
+import org.keycloak.models.mongo.api.types.TypeConverter;
+import org.keycloak.models.mongo.impl.types.EnumToStringConverter;
+import org.keycloak.models.mongo.impl.types.ListConverter;
+import org.keycloak.models.mongo.impl.types.BasicDBListConverter;
+import org.keycloak.models.mongo.impl.types.BasicDBObjectConverter;
+import org.keycloak.models.mongo.impl.types.NoSQLObjectConverter;
+import org.keycloak.models.mongo.impl.types.SimpleConverter;
+import org.keycloak.models.mongo.impl.types.StringToEnumConverter;
+import org.picketlink.common.properties.Property;
+import org.picketlink.common.properties.query.AnnotatedPropertyCriteria;
+import org.picketlink.common.properties.query.PropertyQueries;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MongoDBImpl implements NoSQL {
+
+ private static final Class<?>[] SIMPLE_TYPES = { String.class, Integer.class, Boolean.class, Long.class, Double.class, Character.class, Date.class };
+
+ private final DB database;
+ private static final Logger logger = Logger.getLogger(MongoDBImpl.class);
+
+ private final TypeConverter typeConverter;
+ private ConcurrentMap<Class<? extends NoSQLObject>, ObjectInfo> objectInfoCache =
+ new ConcurrentHashMap<Class<? extends NoSQLObject>, ObjectInfo>();
+
+
+ public MongoDBImpl(DB database, boolean dropDatabaseOnStartup, Class<? extends NoSQLObject>[] managedDataTypes) {
+ this.database = database;
+
+ typeConverter = new TypeConverter();
+
+ for (Class<?> simpleConverterClass : SIMPLE_TYPES) {
+ SimpleConverter converter = new SimpleConverter(simpleConverterClass);
+ typeConverter.addAppObjectConverter(converter);
+ typeConverter.addDBObjectConverter(converter);
+ }
+
+ // Specific converter for ArrayList is added just for performance purposes to avoid recursive converter lookup (most of list impl will be ArrayList)
+ typeConverter.addAppObjectConverter(new ListConverter(typeConverter, ArrayList.class));
+ typeConverter.addAppObjectConverter(new ListConverter(typeConverter, List.class));
+ typeConverter.addDBObjectConverter(new BasicDBListConverter(typeConverter));
+
+ // Enum converters
+ typeConverter.addAppObjectConverter(new EnumToStringConverter());
+ typeConverter.addDBObjectConverter(new StringToEnumConverter());
+
+ for (Class<? extends NoSQLObject> type : managedDataTypes) {
+ getObjectInfo(type);
+ typeConverter.addAppObjectConverter(new NoSQLObjectConverter(this, typeConverter, type));
+ typeConverter.addDBObjectConverter(new BasicDBObjectConverter(this, typeConverter, type));
+ }
+
+ if (dropDatabaseOnStartup) {
+ this.database.dropDatabase();
+ logger.info("Database " + this.database.getName() + " dropped in MongoDB");
+ }
+ }
+
+
+ @Override
+ public void saveObject(NoSQLObject object) {
+ Class<? extends NoSQLObject> clazz = object.getClass();
+
+ // Find annotations for ID, for all the properties and for the name of the collection.
+ ObjectInfo objectInfo = getObjectInfo(clazz);
+
+ // Create instance of BasicDBObject and add all declared properties to it (properties with null value probably should be skipped)
+ BasicDBObject dbObject = typeConverter.convertApplicationObjectToDBObject(object, BasicDBObject.class);
+
+ DBCollection dbCollection = database.getCollection(objectInfo.getDbCollectionName());
+
+ // Decide if we should insert or update (based on presence of oid property in original object)
+ Property<String> oidProperty = objectInfo.getOidProperty();
+ String currentId = oidProperty == null ? null : oidProperty.getValue(object);
+ if (currentId == null) {
+ dbCollection.insert(dbObject);
+
+ // Add oid to value of given object
+ if (oidProperty != null) {
+ oidProperty.setValue(object, dbObject.getString("_id"));
+ }
+ } else {
+ BasicDBObject query = new BasicDBObject("_id", new ObjectId(currentId));
+ dbCollection.update(query, dbObject);
+ }
+ }
+
+
+ @Override
+ public <T extends NoSQLObject> T loadObject(Class<T> type, String oid) {
+ DBCollection dbCollection = getDBCollectionForType(type);
+
+ BasicDBObject idQuery = new BasicDBObject("_id", new ObjectId(oid));
+ DBObject dbObject = dbCollection.findOne(idQuery);
+
+ return typeConverter.convertDBObjectToApplicationObject(dbObject, type);
+ }
+
+
+ @Override
+ public <T extends NoSQLObject> T loadSingleObject(Class<T> type, NoSQLQuery query) {
+ List<T> result = loadObjects(type, query);
+ if (result.size() > 1) {
+ throw new IllegalStateException("There are " + result.size() + " results for type=" + type + ", query=" + query + ". We expect just one");
+ } else if (result.size() == 1) {
+ return result.get(0);
+ } else {
+ // 0 results
+ return null;
+ }
+ }
+
+
+ @Override
+ public <T extends NoSQLObject> List<T> loadObjects(Class<T> type, NoSQLQuery query) {
+ DBCollection dbCollection = getDBCollectionForType(type);
+ BasicDBObject dbQuery = getDBQueryFromQuery(query);
+
+ DBCursor cursor = dbCollection.find(dbQuery);
+
+ return convertCursor(type, cursor);
+ }
+
+
+ @Override
+ public void removeObject(NoSQLObject object) {
+ Class<? extends NoSQLObject> type = object.getClass();
+ ObjectInfo objectInfo = getObjectInfo(type);
+
+ Property<String> idProperty = objectInfo.getOidProperty();
+ String oid = idProperty.getValue(object);
+
+ removeObject(type, oid);
+ }
+
+
+ @Override
+ public void removeObject(Class<? extends NoSQLObject> type, String oid) {
+ NoSQLObject found = loadObject(type, oid);
+ if (found == null) {
+ logger.warn("Object of type: " + type + ", oid: " + oid + " doesn't exist in MongoDB. Skip removal");
+ } else {
+ DBCollection dbCollection = getDBCollectionForType(type);
+ BasicDBObject dbQuery = new BasicDBObject("_id", new ObjectId(oid));
+ dbCollection.remove(dbQuery);
+ logger.info("Object of type: " + type + ", oid: " + oid + " removed from MongoDB.");
+
+ found.afterRemove(this);
+ }
+ }
+
+
+ @Override
+ public void removeObjects(Class<? extends NoSQLObject> type, NoSQLQuery query) {
+ List<? extends NoSQLObject> foundObjects = loadObjects(type, query);
+ if (foundObjects.size() == 0) {
+ logger.info("Not found any objects of type: " + type + ", query: " + query);
+ } else {
+ DBCollection dbCollection = getDBCollectionForType(type);
+ BasicDBObject dbQuery = getDBQueryFromQuery(query);
+ dbCollection.remove(dbQuery);
+ logger.info("Removed " + foundObjects.size() + " objects of type: " + type + ", query: " + query);
+
+ for (NoSQLObject found : foundObjects) {
+ found.afterRemove(this);
+ }
+ }
+ }
+
+
+ @Override
+ public NoSQLQueryBuilder createQueryBuilder() {
+ return new MongoDBQueryBuilder();
+ }
+
+
+ @Override
+ public <S> void pushItemToList(NoSQLObject object, String listPropertyName, S itemToPush) {
+ Class<? extends NoSQLObject> type = object.getClass();
+ ObjectInfo objectInfo = getObjectInfo(type);
+
+ Property<String> oidProperty = getObjectInfo(type).getOidProperty();
+ if (oidProperty == null) {
+ throw new IllegalArgumentException("List pushes not supported for properties without oid");
+ }
+
+ // Add item to list directly in this object
+ Property<Object> listProperty = objectInfo.getPropertyByName(listPropertyName);
+ if (listProperty == null) {
+ throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + object);
+ }
+
+ List<S> list = (List<S>)listProperty.getValue(object);
+ if (list == null) {
+ list = new ArrayList<S>();
+ listProperty.setValue(object, list);
+ }
+ list.add(itemToPush);
+
+ // Push item to DB. We always convert whole list, so it's not so optimal...
+ BasicDBList dbList = typeConverter.convertApplicationObjectToDBObject(list, BasicDBList.class);
+
+ BasicDBObject query = new BasicDBObject("_id", new ObjectId(oidProperty.getValue(object)));
+ BasicDBObject listObject = new BasicDBObject(listPropertyName, dbList);
+ BasicDBObject setCommand = new BasicDBObject("$set", listObject);
+ getDBCollectionForType(type).update(query, setCommand);
+ }
+
+
+ @Override
+ public <S> void pullItemFromList(NoSQLObject object, String listPropertyName, S itemToPull) {
+ Class<? extends NoSQLObject> type = object.getClass();
+ ObjectInfo objectInfo = getObjectInfo(type);
+
+ Property<String> oidProperty = getObjectInfo(type).getOidProperty();
+ if (oidProperty == null) {
+ throw new IllegalArgumentException("List pulls not supported for properties without oid");
+ }
+
+ // Remove item from list directly in this object
+ Property<Object> listProperty = objectInfo.getPropertyByName(listPropertyName);
+ if (listProperty == null) {
+ throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + object);
+ }
+ List<S> list = (List<S>)listProperty.getValue(object);
+
+ // If list is null, we skip both object and DB update
+ if (list != null) {
+ list.remove(itemToPull);
+
+ // Pull item from DB
+ Object dbItemToPull = typeConverter.convertApplicationObjectToDBObject(itemToPull, Object.class);
+ BasicDBObject query = new BasicDBObject("_id", new ObjectId(oidProperty.getValue(object)));
+ BasicDBObject pullObject = new BasicDBObject(listPropertyName, dbItemToPull);
+ BasicDBObject pullCommand = new BasicDBObject("$pull", pullObject);
+ getDBCollectionForType(type).update(query, pullCommand);
+ }
+ }
+
+ // Possibility to add user-defined converters
+ public void addAppObjectConverter(Converter<?, ?> converter) {
+ typeConverter.addAppObjectConverter(converter);
+ }
+
+ public void addDBObjectConverter(Converter<?, ?> converter) {
+ typeConverter.addDBObjectConverter(converter);
+ }
+
+ public ObjectInfo getObjectInfo(Class<? extends NoSQLObject> objectClass) {
+ ObjectInfo objectInfo = objectInfoCache.get(objectClass);
+ if (objectInfo == null) {
+ Property<String> idProperty = PropertyQueries.<String>createQuery(objectClass).addCriteria(new AnnotatedPropertyCriteria(NoSQLId.class)).getFirstResult();
+
+ List<Property<Object>> properties = PropertyQueries.createQuery(objectClass).addCriteria(new AnnotatedPropertyCriteria(NoSQLField.class)).getResultList();
+
+ NoSQLCollection classAnnotation = objectClass.getAnnotation(NoSQLCollection.class);
+
+ String dbCollectionName = classAnnotation==null ? null : classAnnotation.collectionName();
+ objectInfo = new ObjectInfo(objectClass, dbCollectionName, idProperty, properties);
+
+ ObjectInfo existing = objectInfoCache.putIfAbsent(objectClass, objectInfo);
+ if (existing != null) {
+ objectInfo = existing;
+ }
+ }
+
+ return objectInfo;
+ }
+
+ private <T extends NoSQLObject> List<T> convertCursor(Class<T> type, DBCursor cursor) {
+ List<T> result = new ArrayList<T>();
+
+ try {
+ for (DBObject dbObject : cursor) {
+ T converted = typeConverter.convertDBObjectToApplicationObject(dbObject, type);
+ result.add(converted);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return result;
+ }
+
+ private DBCollection getDBCollectionForType(Class<? extends NoSQLObject> type) {
+ ObjectInfo objectInfo = getObjectInfo(type);
+ return database.getCollection(objectInfo.getDbCollectionName());
+ }
+
+ private BasicDBObject getDBQueryFromQuery(NoSQLQuery query) {
+ Map<String, Object> queryAttributes = query.getQueryAttributes();
+ BasicDBObject dbQuery = new BasicDBObject();
+ for (Map.Entry<String, Object> queryAttr : queryAttributes.entrySet()) {
+ dbQuery.append(queryAttr.getKey(), queryAttr.getValue());
+ }
+ return dbQuery;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBQueryBuilder.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBQueryBuilder.java
new file mode 100644
index 0000000..f56c799
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBQueryBuilder.java
@@ -0,0 +1,38 @@
+package org.keycloak.models.mongo.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.mongodb.BasicDBObject;
+import org.bson.types.ObjectId;
+import org.keycloak.models.mongo.api.query.NoSQLQueryBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MongoDBQueryBuilder extends NoSQLQueryBuilder {
+
+ protected MongoDBQueryBuilder() {};
+
+ @Override
+ public NoSQLQueryBuilder inCondition(String name, List<?> values) {
+ if (values == null) {
+ values = new LinkedList<Object>();
+ }
+
+ if ("_id".equals(name)) {
+ // we need to convert Strings to ObjectID
+ List<ObjectId> objIds = new ArrayList<ObjectId>();
+ for (Object object : values) {
+ ObjectId objectId = new ObjectId(object.toString());
+ objIds.add(objectId);
+ }
+ values = objIds;
+ }
+
+ BasicDBObject inObject = new BasicDBObject("$in", values);
+ put(name, inObject);
+ return this;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/ObjectInfo.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/ObjectInfo.java
new file mode 100644
index 0000000..ae548a6
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/ObjectInfo.java
@@ -0,0 +1,56 @@
+package org.keycloak.models.mongo.impl;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.picketlink.common.properties.Property;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ObjectInfo {
+
+ private final Class<? extends NoSQLObject> objectClass;
+
+ private final String dbCollectionName;
+
+ private final Property<String> oidProperty;
+
+ private final Map<String, Property<Object>> properties;
+
+ public ObjectInfo(Class<? extends NoSQLObject> objectClass, String dbCollectionName, Property<String> oidProperty, List<Property<Object>> properties) {
+ this.objectClass = objectClass;
+ this.dbCollectionName = dbCollectionName;
+ this.oidProperty = oidProperty;
+
+ Map<String, Property<Object>> props= new HashMap<String, Property<Object>>();
+ for (Property<Object> property : properties) {
+ props.put(property.getName(), property);
+ }
+ this.properties = Collections.unmodifiableMap(props);
+ }
+
+ public Class<? extends NoSQLObject> getObjectClass() {
+ return objectClass;
+ }
+
+ public String getDbCollectionName() {
+ return dbCollectionName;
+ }
+
+ public Property<String> getOidProperty() {
+ return oidProperty;
+ }
+
+ public Collection<Property<Object>> getProperties() {
+ return properties.values();
+ }
+
+ public Property<Object> getPropertyByName(String propertyName) {
+ return properties.get(propertyName);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBListConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBListConverter.java
new file mode 100644
index 0000000..896257f
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBListConverter.java
@@ -0,0 +1,73 @@
+package org.keycloak.models.mongo.impl.types;
+
+import java.util.ArrayList;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObject;
+import org.keycloak.models.mongo.api.types.Converter;
+import org.keycloak.models.mongo.api.types.TypeConverter;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class BasicDBListConverter implements Converter<BasicDBList, ArrayList> {
+
+ private final TypeConverter typeConverter;
+
+ public BasicDBListConverter(TypeConverter typeConverter) {
+ this.typeConverter = typeConverter;
+ }
+
+ @Override
+ public ArrayList convertObject(BasicDBList dbList) {
+ ArrayList<Object> appObjects = new ArrayList<Object>();
+ Class<?> expectedListElementType = null;
+ for (Object dbObject : dbList) {
+
+ if (expectedListElementType == null) {
+ expectedListElementType = findExpectedListElementType(dbObject);
+ }
+
+ appObjects.add(typeConverter.convertDBObjectToApplicationObject(dbObject, expectedListElementType));
+ }
+ return appObjects;
+ }
+
+ @Override
+ public Class<? extends BasicDBList> getConverterObjectType() {
+ return BasicDBList.class;
+ }
+
+ @Override
+ public Class<ArrayList> getExpectedReturnType() {
+ return ArrayList.class;
+ }
+
+ private Class<?> findExpectedListElementType(Object dbObject) {
+ if (dbObject instanceof BasicDBObject) {
+ BasicDBObject basicDBObject = (BasicDBObject) dbObject;
+ String type = (String)basicDBObject.get(ListConverter.OBJECT_TYPE);
+ if (type == null) {
+ throw new IllegalStateException("Not found OBJECT_TYPE key inside object " + dbObject);
+ }
+ basicDBObject.remove(ListConverter.OBJECT_TYPE);
+
+ try {
+ return Class.forName(type);
+ } catch (ClassNotFoundException cnfe) {
+ throw new RuntimeException(cnfe);
+ }
+ } else {
+ // Special case (if we have String like "org.keycloak.Gender###MALE" we expect that substring before ### is className
+ if (String.class.equals(dbObject.getClass())) {
+ String dbObjString = (String)dbObject;
+ if (dbObjString.contains(ClassCache.SPLIT)) {
+ String className = dbObjString.substring(0, dbObjString.indexOf(ClassCache.SPLIT));
+ return ClassCache.getInstance().getOrLoadClass(className);
+ }
+ }
+
+ return dbObject.getClass();
+ }
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectConverter.java
new file mode 100644
index 0000000..a423652
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectConverter.java
@@ -0,0 +1,102 @@
+package org.keycloak.models.mongo.impl.types;
+
+import com.mongodb.BasicDBObject;
+import org.jboss.logging.Logger;
+import org.keycloak.models.mongo.api.AttributedNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.types.Converter;
+import org.keycloak.models.mongo.api.types.TypeConverter;
+import org.keycloak.models.mongo.impl.MongoDBImpl;
+import org.keycloak.models.mongo.impl.ObjectInfo;
+import org.picketlink.common.properties.Property;
+import org.picketlink.common.reflection.Types;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class BasicDBObjectConverter<S extends NoSQLObject> implements Converter<BasicDBObject, S> {
+
+ private static final Logger logger = Logger.getLogger(BasicDBObjectConverter.class);
+
+ private final MongoDBImpl mongoDBImpl;
+ private final TypeConverter typeConverter;
+ private final Class<S> expectedNoSQLObjectType;
+
+ public BasicDBObjectConverter(MongoDBImpl mongoDBImpl, TypeConverter typeConverter, Class<S> expectedNoSQLObjectType) {
+ this.mongoDBImpl = mongoDBImpl;
+ this.typeConverter = typeConverter;
+ this.expectedNoSQLObjectType = expectedNoSQLObjectType;
+ }
+
+ @Override
+ public S convertObject(BasicDBObject dbObject) {
+ if (dbObject == null) {
+ return null;
+ }
+
+ ObjectInfo objectInfo = mongoDBImpl.getObjectInfo(expectedNoSQLObjectType);
+
+ S object;
+ try {
+ object = expectedNoSQLObjectType.newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ for (String key : dbObject.keySet()) {
+ Object value = dbObject.get(key);
+ Property<Object> property;
+
+ if ("_id".equals(key)) {
+ // Current property is "id"
+ Property<String> idProperty = objectInfo.getOidProperty();
+ if (idProperty != null) {
+ idProperty.setValue(object, value.toString());
+ }
+
+ } else if ((property = objectInfo.getPropertyByName(key)) != null) {
+ // It's declared property with @DBField annotation
+ setPropertyValue(object, value, property);
+
+ } else if (object instanceof AttributedNoSQLObject) {
+ // It's attributed object and property is not declared, so we will call setAttribute
+ ((AttributedNoSQLObject)object).setAttribute(key, value.toString());
+
+ } else {
+ // Show warning if it's unknown
+ logger.warn("Property with key " + key + " not known for type " + expectedNoSQLObjectType);
+ }
+ }
+
+ return object;
+ }
+
+ private void setPropertyValue(NoSQLObject object, Object valueFromDB, Property property) {
+ if (valueFromDB == null) {
+ property.setValue(object, null);
+ return;
+ }
+
+ Class<?> expectedReturnType = property.getJavaClass();
+ // handle primitives
+ expectedReturnType = Types.boxedClass(expectedReturnType);
+
+ Object appObject = typeConverter.convertDBObjectToApplicationObject(valueFromDB, expectedReturnType);
+ if (Types.boxedClass(property.getJavaClass()).isAssignableFrom(appObject.getClass())) {
+ property.setValue(object, appObject);
+ } else {
+ throw new IllegalStateException("Converted object " + appObject + " is not of type " + expectedReturnType +
+ ". So can't be assigned as property " + property.getName() + " of " + object.getClass());
+ }
+ }
+
+ @Override
+ public Class<? extends BasicDBObject> getConverterObjectType() {
+ return BasicDBObject.class;
+ }
+
+ @Override
+ public Class<S> getExpectedReturnType() {
+ return expectedNoSQLObjectType;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ClassCache.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ClassCache.java
new file mode 100644
index 0000000..891ccdd
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ClassCache.java
@@ -0,0 +1,37 @@
+package org.keycloak.models.mongo.impl.types;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Helper class for caching of classNames to actual classes (Should help a bit to avoid expensive reflection calls)
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClassCache {
+
+ public static final String SPLIT = "###";
+ private static final ClassCache INSTANCE = new ClassCache();
+
+ private ConcurrentMap<String, Class<?>> cache = new ConcurrentHashMap<String, Class<?>>();
+
+ private ClassCache() {};
+
+ public static ClassCache getInstance() {
+ return INSTANCE;
+ }
+
+ public Class<?> getOrLoadClass(String className) {
+ Class<?> clazz = cache.get(className);
+ if (clazz == null) {
+ try {
+ clazz = Class.forName(className);
+ cache.putIfAbsent(className, clazz);
+ } catch (ClassNotFoundException cnfe) {
+ throw new RuntimeException(cnfe);
+ }
+ }
+ return clazz;
+ }
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/EnumToStringConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/EnumToStringConverter.java
new file mode 100644
index 0000000..2a800df
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/EnumToStringConverter.java
@@ -0,0 +1,26 @@
+package org.keycloak.models.mongo.impl.types;
+
+import org.keycloak.models.mongo.api.types.Converter;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class EnumToStringConverter implements Converter<Enum, String> {
+
+ // It will be saved in form of "org.keycloak.Gender#MALE" so it's possible to parse enumType out of it
+ @Override
+ public String convertObject(Enum objectToConvert) {
+ String className = objectToConvert.getClass().getName();
+ return className + ClassCache.SPLIT + objectToConvert.toString();
+ }
+
+ @Override
+ public Class<? extends Enum> getConverterObjectType() {
+ return Enum.class;
+ }
+
+ @Override
+ public Class<String> getExpectedReturnType() {
+ return String.class;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ListConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ListConverter.java
new file mode 100644
index 0000000..8b72ca2
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ListConverter.java
@@ -0,0 +1,52 @@
+package org.keycloak.models.mongo.impl.types;
+
+import java.util.List;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObject;
+import org.keycloak.models.mongo.api.types.Converter;
+import org.keycloak.models.mongo.api.types.TypeConverter;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ListConverter<T extends List> implements Converter<T, BasicDBList> {
+
+ // Key for ObjectType field, which points to actual Java type of element objects inside list
+ static final String OBJECT_TYPE = "OBJECT_TYPE";
+
+ private final TypeConverter typeConverter;
+ private final Class<T> listType;
+
+ public ListConverter(TypeConverter typeConverter, Class<T> listType) {
+ this.typeConverter = typeConverter;
+ this.listType = listType;
+ }
+
+ @Override
+ public BasicDBList convertObject(T appObjectsList) {
+ BasicDBList dbObjects = new BasicDBList();
+ for (Object appObject : appObjectsList) {
+ Object dbObject = typeConverter.convertApplicationObjectToDBObject(appObject, Object.class);
+
+ // We need to add OBJECT_TYPE key to object, so we can retrieve correct Java type of object during load of this list
+ if (dbObject instanceof BasicDBObject) {
+ BasicDBObject basicDBObject = (BasicDBObject)dbObject;
+ basicDBObject.put(OBJECT_TYPE, appObject.getClass().getName());
+ }
+
+ dbObjects.add(dbObject);
+ }
+ return dbObjects;
+ }
+
+ @Override
+ public Class<? extends T> getConverterObjectType() {
+ return listType;
+ }
+
+ @Override
+ public Class<BasicDBList> getExpectedReturnType() {
+ return BasicDBList.class;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/NoSQLObjectConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/NoSQLObjectConverter.java
new file mode 100644
index 0000000..f7be7ae
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/NoSQLObjectConverter.java
@@ -0,0 +1,66 @@
+package org.keycloak.models.mongo.impl.types;
+
+import java.util.Collection;
+import java.util.Map;
+
+import com.mongodb.BasicDBObject;
+import org.keycloak.models.mongo.api.AttributedNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.types.Converter;
+import org.keycloak.models.mongo.api.types.TypeConverter;
+import org.keycloak.models.mongo.impl.MongoDBImpl;
+import org.keycloak.models.mongo.impl.ObjectInfo;
+import org.picketlink.common.properties.Property;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class NoSQLObjectConverter<T extends NoSQLObject> implements Converter<T, BasicDBObject> {
+
+ private final MongoDBImpl mongoDBImpl;
+ private final TypeConverter typeConverter;
+ private final Class<T> expectedNoSQLObjectType;
+
+ public NoSQLObjectConverter(MongoDBImpl mongoDBImpl, TypeConverter typeConverter, Class<T> expectedNoSQLObjectType) {
+ this.mongoDBImpl = mongoDBImpl;
+ this.typeConverter = typeConverter;
+ this.expectedNoSQLObjectType = expectedNoSQLObjectType;
+ }
+
+ @Override
+ public BasicDBObject convertObject(T applicationObject) {
+ ObjectInfo objectInfo = mongoDBImpl.getObjectInfo(applicationObject.getClass());
+
+ // Create instance of BasicDBObject and add all declared properties to it (properties with null value probably should be skipped)
+ BasicDBObject dbObject = new BasicDBObject();
+ Collection<Property<Object>> props = objectInfo.getProperties();
+ for (Property<Object> property : props) {
+ String propName = property.getName();
+ Object propValue = property.getValue(applicationObject);
+
+ Object dbValue = propValue == null ? null : typeConverter.convertApplicationObjectToDBObject(propValue, Object.class);
+ dbObject.put(propName, dbValue);
+ }
+
+ // Adding attributes
+ if (applicationObject instanceof AttributedNoSQLObject) {
+ AttributedNoSQLObject attributedObject = (AttributedNoSQLObject)applicationObject;
+ Map<String, String> attributes = attributedObject.getAttributes();
+ for (Map.Entry<String, String> attribute : attributes.entrySet()) {
+ dbObject.append(attribute.getKey(), attribute.getValue());
+ }
+ }
+
+ return dbObject;
+ }
+
+ @Override
+ public Class<? extends T> getConverterObjectType() {
+ return expectedNoSQLObjectType;
+ }
+
+ @Override
+ public Class<BasicDBObject> getExpectedReturnType() {
+ return BasicDBObject.class;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/SimpleConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/SimpleConverter.java
new file mode 100644
index 0000000..5ba1de5
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/SimpleConverter.java
@@ -0,0 +1,30 @@
+package org.keycloak.models.mongo.impl.types;
+
+import org.keycloak.models.mongo.api.types.Converter;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SimpleConverter<T> implements Converter<T, T> {
+
+ private final Class<T> expectedType;
+
+ public SimpleConverter(Class<T> expectedType) {
+ this.expectedType = expectedType;
+ }
+
+ @Override
+ public T convertObject(T objectToConvert) {
+ return objectToConvert;
+ }
+
+ @Override
+ public Class<? extends T> getConverterObjectType() {
+ return expectedType;
+ }
+
+ @Override
+ public Class<T> getExpectedReturnType() {
+ return expectedType;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/StringToEnumConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/StringToEnumConverter.java
new file mode 100644
index 0000000..0c948ec
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/StringToEnumConverter.java
@@ -0,0 +1,32 @@
+package org.keycloak.models.mongo.impl.types;
+
+import org.keycloak.models.mongo.api.types.Converter;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class StringToEnumConverter implements Converter<String, Enum> {
+
+ @Override
+ public Enum convertObject(String objectToConvert) {
+ int index = objectToConvert.indexOf(ClassCache.SPLIT);
+ if (index == -1) {
+ throw new IllegalStateException("Can't convert enum type with value " + objectToConvert);
+ }
+
+ String className = objectToConvert.substring(0, index);
+ String enumValue = objectToConvert.substring(index + 3);
+ Class<? extends Enum> clazz = (Class<? extends Enum>)ClassCache.getInstance().getOrLoadClass(className);
+ return Enum.valueOf(clazz, enumValue);
+ }
+
+ @Override
+ public Class<? extends String> getConverterObjectType() {
+ return String.class;
+ }
+
+ @Override
+ public Class<Enum> getExpectedReturnType() {
+ return Enum.class;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java
new file mode 100644
index 0000000..49bcd31
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ApplicationAdapter.java
@@ -0,0 +1,284 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.data.ApplicationData;
+import org.keycloak.models.mongo.keycloak.data.RoleData;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ApplicationAdapter implements ApplicationModel {
+
+ private final ApplicationData application;
+ private final NoSQL noSQL;
+
+ private UserData resourceUser;
+
+ public ApplicationAdapter(ApplicationData applicationData, NoSQL noSQL) {
+ this.application = applicationData;
+ this.noSQL = noSQL;
+ }
+
+ @Override
+ public void updateApplication() {
+ noSQL.saveObject(application);
+ }
+
+ @Override
+ public UserModel getApplicationUser() {
+ // This is not thread-safe. Assumption is that ApplicationAdapter instance is per-client object
+ if (resourceUser == null) {
+ resourceUser = noSQL.loadObject(UserData.class, application.getResourceUserId());
+ }
+
+ return resourceUser != null ? new UserAdapter(resourceUser, noSQL) : null;
+ }
+
+ @Override
+ public String getId() {
+ return application.getId();
+ }
+
+ @Override
+ public String getName() {
+ return application.getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ application.setName(name);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return application.isEnabled();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ application.setEnabled(enabled);
+ }
+
+ @Override
+ public boolean isSurrogateAuthRequired() {
+ return application.isSurrogateAuthRequired();
+ }
+
+ @Override
+ public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
+ application.setSurrogateAuthRequired(surrogateAuthRequired);
+ }
+
+ @Override
+ public String getManagementUrl() {
+ return application.getManagementUrl();
+ }
+
+ @Override
+ public void setManagementUrl(String url) {
+ application.setManagementUrl(url);
+ }
+
+ @Override
+ public void setBaseUrl(String url) {
+ application.setBaseUrl(url);
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return application.getBaseUrl();
+ }
+
+ @Override
+ public RoleAdapter getRole(String name) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("name", name)
+ .andCondition("applicationId", getId())
+ .build();
+ RoleData role = noSQL.loadSingleObject(RoleData.class, query);
+ if (role == null) {
+ return null;
+ } else {
+ return new RoleAdapter(role, noSQL);
+ }
+ }
+
+ @Override
+ public RoleModel getRoleById(String id) {
+ RoleData role = noSQL.loadObject(RoleData.class, id);
+ if (role == null) {
+ return null;
+ } else {
+ return new RoleAdapter(role, noSQL);
+ }
+ }
+
+ @Override
+ public void grantRole(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+ noSQL.pushItemToList(userData, "roleIds", role.getId());
+ }
+
+ @Override
+ public boolean hasRole(UserModel user, String role) {
+ RoleModel roleModel = getRole(role);
+ return hasRole(user, roleModel);
+ }
+
+ @Override
+ public boolean hasRole(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+
+ List<String> roleIds = userData.getRoleIds();
+ String roleId = role.getId();
+ if (roleIds != null) {
+ for (String currentId : roleIds) {
+ if (roleId.equals(currentId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public RoleAdapter addRole(String name) {
+ if (getRole(name) != null) {
+ throw new IllegalArgumentException("Role " + name + " already exists");
+ }
+
+ RoleData roleData = new RoleData();
+ roleData.setName(name);
+ roleData.setApplicationId(getId());
+
+ noSQL.saveObject(roleData);
+ return new RoleAdapter(roleData, noSQL);
+ }
+
+ @Override
+ public List<RoleModel> getRoles() {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("applicationId", getId())
+ .build();
+ List<RoleData> roles = noSQL.loadObjects(RoleData.class, query);
+
+ List<RoleModel> result = new ArrayList<RoleModel>();
+ for (RoleData role : roles) {
+ result.add(new RoleAdapter(role, noSQL));
+ }
+
+ return result;
+ }
+
+ // Static so that it can be used from RealmAdapter as well
+ static List<RoleData> getAllRolesOfUser(UserModel user, NoSQL noSQL) {
+ UserData userData = ((UserAdapter)user).getUser();
+ List<String> roleIds = userData.getRoleIds();
+
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .inCondition("_id", roleIds)
+ .build();
+ return noSQL.loadObjects(RoleData.class, query);
+ }
+
+ @Override
+ public Set<String> getRoleMappingValues(UserModel user) {
+ Set<String> result = new HashSet<String>();
+ List<RoleData> roles = getAllRolesOfUser(user, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getId().equals(role.getApplicationId())) {
+ result.add(role.getName());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<RoleModel> getRoleMappings(UserModel user) {
+ List<RoleModel> result = new ArrayList<RoleModel>();
+ List<RoleData> roles = getAllRolesOfUser(user, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getId().equals(role.getApplicationId())) {
+ result.add(new RoleAdapter(role, noSQL));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void deleteRoleMapping(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+ noSQL.pullItemFromList(userData, "roleIds", role.getId());
+ }
+
+ @Override
+ public void addScopeMapping(UserModel agent, String roleName) {
+ RoleAdapter role = getRole(roleName);
+ if (role == null) {
+ throw new RuntimeException("Role not found");
+ }
+
+ addScopeMapping(agent, role);
+ }
+
+ @Override
+ public void addScopeMapping(UserModel agent, RoleModel role) {
+ UserData userData = ((UserAdapter)agent).getUser();
+ noSQL.pushItemToList(userData, "scopeIds", role.getId());
+ }
+
+ @Override
+ public void deleteScopeMapping(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+ noSQL.pullItemFromList(userData, "scopeIds", role.getId());
+ }
+
+ // Static so that it can be used from RealmAdapter as well
+ static List<RoleData> getAllScopesOfUser(UserModel user, NoSQL noSQL) {
+ UserData userData = ((UserAdapter)user).getUser();
+ List<String> roleIds = userData.getScopeIds();
+
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .inCondition("_id", roleIds)
+ .build();
+ return noSQL.loadObjects(RoleData.class, query);
+ }
+
+ @Override
+ public Set<String> getScopeMappingValues(UserModel agent) {
+ Set<String> result = new HashSet<String>();
+ List<RoleData> roles = getAllScopesOfUser(agent, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getId().equals(role.getApplicationId())) {
+ result.add(role.getName());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<RoleModel> getScopeMappings(UserModel agent) {
+ List<RoleModel> result = new ArrayList<RoleModel>();
+ List<RoleData> roles = getAllScopesOfUser(agent, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getId().equals(role.getApplicationId())) {
+ result.add(new RoleAdapter(role, noSQL));
+ }
+ }
+ return result;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoDBSessionFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoDBSessionFactory.java
new file mode 100644
index 0000000..b1ac509
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoDBSessionFactory.java
@@ -0,0 +1,70 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import java.net.UnknownHostException;
+
+import com.mongodb.DB;
+import com.mongodb.MongoClient;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.keycloak.data.ApplicationData;
+import org.keycloak.models.mongo.keycloak.data.OAuthClientData;
+import org.keycloak.models.mongo.keycloak.data.RealmData;
+import org.keycloak.models.mongo.keycloak.data.RequiredCredentialData;
+import org.keycloak.models.mongo.keycloak.data.RoleData;
+import org.keycloak.models.mongo.keycloak.data.SocialLinkData;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+import org.keycloak.models.mongo.impl.MongoDBImpl;
+import org.keycloak.models.mongo.keycloak.data.credentials.OTPData;
+import org.keycloak.models.mongo.keycloak.data.credentials.PasswordData;
+
+/**
+ * NoSQL implementation based on MongoDB
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MongoDBSessionFactory implements KeycloakSessionFactory {
+ protected static final Logger logger = Logger.getLogger(MongoDBSessionFactory.class);
+
+ private static final Class<? extends NoSQLObject>[] MANAGED_DATA_TYPES = (Class<? extends NoSQLObject>[])new Class<?>[] {
+ RealmData.class,
+ UserData.class,
+ RoleData.class,
+ RequiredCredentialData.class,
+ PasswordData.class,
+ OTPData.class,
+ SocialLinkData.class,
+ ApplicationData.class,
+ OAuthClientData.class
+ };
+
+ private final MongoClient mongoClient;
+ private final NoSQL mongoDB;
+
+ public MongoDBSessionFactory(String host, int port, String dbName, boolean dropDatabaseOnStartup) {
+ logger.info(String.format("Going to use MongoDB database. host: %s, port: %d, databaseName: %s, removeAllObjectsAtStartup: %b", host, port, dbName, dropDatabaseOnStartup));
+ try {
+ // TODO: authentication support
+ mongoClient = new MongoClient(host, port);
+
+ DB db = mongoClient.getDB(dbName);
+ mongoDB = new MongoDBImpl(db, dropDatabaseOnStartup, MANAGED_DATA_TYPES);
+
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public KeycloakSession createSession() {
+ return new NoSQLSession(mongoDB);
+ }
+
+ @Override
+ public void close() {
+ logger.info("Closing MongoDB client");
+ mongoClient.close();
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLSession.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLSession.java
new file mode 100644
index 0000000..2bc413d
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLSession.java
@@ -0,0 +1,86 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.data.RealmData;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.utils.KeycloakSessionUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class NoSQLSession implements KeycloakSession {
+
+ private static final NoSQLTransaction PLACEHOLDER = new NoSQLTransaction();
+ private final NoSQL noSQL;
+
+ public NoSQLSession(NoSQL noSQL) {
+ this.noSQL = noSQL;
+ }
+
+ @Override
+ public KeycloakTransaction getTransaction() {
+ return PLACEHOLDER;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public RealmModel createRealm(String name) {
+ return createRealm(KeycloakSessionUtils.generateId(), name);
+ }
+
+ @Override
+ public RealmModel createRealm(String id, String name) {
+ if (getRealm(id) != null) {
+ throw new IllegalStateException("Realm with id '" + id + "' already exists");
+ }
+
+ RealmData newRealm = new RealmData();
+ newRealm.setId(id);
+ newRealm.setName(name);
+
+ noSQL.saveObject(newRealm);
+
+ RealmAdapter realm = new RealmAdapter(newRealm, noSQL);
+ return realm;
+ }
+
+ @Override
+ public RealmModel getRealm(String id) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("id", id)
+ .build();
+ RealmData realmData = noSQL.loadSingleObject(RealmData.class, query);
+ return realmData != null ? new RealmAdapter(realmData, noSQL) : null;
+ }
+
+ @Override
+ public List<RealmModel> getRealms(UserModel admin) {
+ String userId = ((UserAdapter)admin).getUser().getId();
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmAdmins", userId)
+ .build();
+ List<RealmData> realms = noSQL.loadObjects(RealmData.class, query);
+
+ List<RealmModel> results = new ArrayList<RealmModel>();
+ for (RealmData realmData : realms) {
+ results.add(new RealmAdapter(realmData, noSQL));
+ }
+ return results;
+ }
+
+ @Override
+ public void deleteRealm(RealmModel realm) {
+ String oid = ((RealmAdapter)realm).getOid();
+ noSQL.removeObject(RealmData.class, oid);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLTransaction.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLTransaction.java
new file mode 100644
index 0000000..3d16635
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLTransaction.java
@@ -0,0 +1,39 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import org.keycloak.models.KeycloakTransaction;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class NoSQLTransaction implements KeycloakTransaction {
+
+ @Override
+ public void begin() {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public void commit() {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public void rollback() {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return false; //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public boolean isActive() {
+ return true;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java
new file mode 100644
index 0000000..34f455e
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/OAuthClientAdapter.java
@@ -0,0 +1,54 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import org.keycloak.models.OAuthClientModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.keycloak.data.OAuthClientData;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OAuthClientAdapter implements OAuthClientModel {
+
+ private final OAuthClientData delegate;
+ private UserAdapter oauthAgent;
+ private final NoSQL noSQL;
+
+ public OAuthClientAdapter(OAuthClientData oauthClientData, UserAdapter oauthAgent, NoSQL noSQL) {
+ this.delegate = oauthClientData;
+ this.oauthAgent = oauthAgent;
+ this.noSQL = noSQL;
+ }
+
+ public OAuthClientAdapter(OAuthClientData oauthClientData, NoSQL noSQL) {
+ this.delegate = oauthClientData;
+ this.noSQL = noSQL;
+ }
+
+ @Override
+ public String getId() {
+ return delegate.getId();
+ }
+
+ @Override
+ public UserModel getOAuthAgent() {
+ // This is not thread-safe. Assumption is that OAuthClientAdapter instance is per-client object
+ if (oauthAgent == null) {
+ UserData user = noSQL.loadObject(UserData.class, delegate.getOauthAgentId());
+ oauthAgent = user!=null ? new UserAdapter(user, noSQL) : null;
+ }
+ return oauthAgent;
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return delegate.getBaseUrl();
+ }
+
+ @Override
+ public void setBaseUrl(String base) {
+ delegate.setBaseUrl(base);
+ noSQL.saveObject(delegate);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
new file mode 100644
index 0000000..837f985
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -0,0 +1,862 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.bouncycastle.openssl.PEMWriter;
+import org.keycloak.PemUtils;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.OAuthClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RequiredCredentialModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.SocialLinkModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.mongo.api.query.NoSQLQueryBuilder;
+import org.keycloak.models.mongo.keycloak.data.OAuthClientData;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.credentials.PasswordCredentialHandler;
+import org.keycloak.models.mongo.keycloak.credentials.TOTPCredentialHandler;
+import org.keycloak.models.mongo.keycloak.data.ApplicationData;
+import org.keycloak.models.mongo.keycloak.data.RealmData;
+import org.keycloak.models.mongo.keycloak.data.RequiredCredentialData;
+import org.keycloak.models.mongo.keycloak.data.RoleData;
+import org.keycloak.models.mongo.keycloak.data.SocialLinkData;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+import org.picketlink.idm.credential.Credentials;
+import org.picketlink.idm.model.sample.User;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RealmAdapter implements RealmModel {
+
+ private final RealmData realm;
+ private final NoSQL noSQL;
+
+ protected volatile transient PublicKey publicKey;
+ protected volatile transient PrivateKey privateKey;
+
+ // TODO: likely shouldn't be static. And ATM, just empty map is passed -> It's not possible to configure stuff like PasswordEncoder etc.
+ private static PasswordCredentialHandler passwordCredentialHandler = new PasswordCredentialHandler(new HashMap<String, Object>());
+ private static TOTPCredentialHandler totpCredentialHandler = new TOTPCredentialHandler(new HashMap<String, Object>());
+
+ public RealmAdapter(RealmData realmData, NoSQL noSQL) {
+ this.realm = realmData;
+ this.noSQL = noSQL;
+ }
+
+ protected String getOid() {
+ return realm.getOid();
+ }
+
+ @Override
+ public String getId() {
+ return realm.getId();
+ }
+
+ @Override
+ public String getName() {
+ return realm.getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ realm.setName(name);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return realm.isEnabled();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ realm.setEnabled(enabled);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isSocial() {
+ return realm.isSocial();
+ }
+
+ @Override
+ public void setSocial(boolean social) {
+ realm.setSocial(social);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isAutomaticRegistrationAfterSocialLogin() {
+ return realm.isAutomaticRegistrationAfterSocialLogin();
+ }
+
+ @Override
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
+ realm.setAutomaticRegistrationAfterSocialLogin(automaticRegistrationAfterSocialLogin);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isSslNotRequired() {
+ return realm.isSslNotRequired();
+ }
+
+ @Override
+ public void setSslNotRequired(boolean sslNotRequired) {
+ realm.setSslNotRequired(sslNotRequired);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isCookieLoginAllowed() {
+ return realm.isCookieLoginAllowed();
+ }
+
+ @Override
+ public void setCookieLoginAllowed(boolean cookieLoginAllowed) {
+ realm.setCookieLoginAllowed(cookieLoginAllowed);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isRegistrationAllowed() {
+ return realm.isRegistrationAllowed();
+ }
+
+ @Override
+ public void setRegistrationAllowed(boolean registrationAllowed) {
+ realm.setRegistrationAllowed(registrationAllowed);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isVerifyEmail() {
+ return realm.isVerifyEmail();
+ }
+
+ @Override
+ public void setVerifyEmail(boolean verifyEmail) {
+ realm.setVerifyEmail(verifyEmail);
+ updateRealm();
+ }
+
+ @Override
+ public boolean isResetPasswordAllowed() {
+ return realm.isResetPasswordAllowed();
+ }
+
+ @Override
+ public void setResetPasswordAllowed(boolean resetPassword) {
+ realm.setResetPasswordAllowed(resetPassword);
+ updateRealm();
+ }
+
+ @Override
+ public int getTokenLifespan() {
+ return realm.getTokenLifespan();
+ }
+
+ @Override
+ public void setTokenLifespan(int tokenLifespan) {
+ realm.setTokenLifespan(tokenLifespan);
+ updateRealm();
+ }
+
+ @Override
+ public int getAccessCodeLifespan() {
+ return realm.getAccessCodeLifespan();
+ }
+
+ @Override
+ public void setAccessCodeLifespan(int accessCodeLifespan) {
+ realm.setAccessCodeLifespan(accessCodeLifespan);
+ updateRealm();
+ }
+
+ @Override
+ public int getAccessCodeLifespanUserAction() {
+ return realm.getAccessCodeLifespanUserAction();
+ }
+
+ @Override
+ public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
+ realm.setAccessCodeLifespanUserAction(accessCodeLifespanUserAction);
+ updateRealm();
+ }
+
+ @Override
+ public String getPublicKeyPem() {
+ return realm.getPublicKeyPem();
+ }
+
+ @Override
+ public void setPublicKeyPem(String publicKeyPem) {
+ realm.setPublicKeyPem(publicKeyPem);
+ this.publicKey = null;
+ updateRealm();
+ }
+
+ @Override
+ public String getPrivateKeyPem() {
+ return realm.getPrivateKeyPem();
+ }
+
+ @Override
+ public void setPrivateKeyPem(String privateKeyPem) {
+ realm.setPrivateKeyPem(privateKeyPem);
+ this.privateKey = null;
+ updateRealm();
+ }
+
+ @Override
+ public PublicKey getPublicKey() {
+ if (publicKey != null) return publicKey;
+ String pem = getPublicKeyPem();
+ if (pem != null) {
+ try {
+ publicKey = PemUtils.decodePublicKey(pem);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return publicKey;
+ }
+
+ @Override
+ public void setPublicKey(PublicKey publicKey) {
+ this.publicKey = publicKey;
+ StringWriter writer = new StringWriter();
+ PEMWriter pemWriter = new PEMWriter(writer);
+ try {
+ pemWriter.writeObject(publicKey);
+ pemWriter.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ String s = writer.toString();
+ setPublicKeyPem(PemUtils.removeBeginEnd(s));
+ }
+
+ @Override
+ public PrivateKey getPrivateKey() {
+ if (privateKey != null) return privateKey;
+ String pem = getPrivateKeyPem();
+ if (pem != null) {
+ try {
+ privateKey = PemUtils.decodePrivateKey(pem);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return privateKey;
+ }
+
+ @Override
+ public void setPrivateKey(PrivateKey privateKey) {
+ this.privateKey = privateKey;
+ StringWriter writer = new StringWriter();
+ PEMWriter pemWriter = new PEMWriter(writer);
+ try {
+ pemWriter.writeObject(privateKey);
+ pemWriter.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ String s = writer.toString();
+ setPrivateKeyPem(PemUtils.removeBeginEnd(s));
+ }
+
+ @Override
+ public UserAdapter getUser(String name) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("loginName", name)
+ .andCondition("realmId", getOid())
+ .build();
+ UserData user = noSQL.loadSingleObject(UserData.class, query);
+
+ if (user == null) {
+ return null;
+ } else {
+ return new UserAdapter(user, noSQL);
+ }
+ }
+
+ @Override
+ public UserAdapter addUser(String username) {
+ if (getUser(username) != null) {
+ throw new IllegalArgumentException("User " + username + " already exists");
+ }
+
+ UserData userData = new UserData();
+ userData.setLoginName(username);
+ userData.setEnabled(true);
+ userData.setRealmId(getOid());
+
+ noSQL.saveObject(userData);
+ return new UserAdapter(userData, noSQL);
+ }
+
+ // This method doesn't exists on interface actually
+ public void removeUser(String name) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("loginName", name)
+ .andCondition("realmId", getOid())
+ .build();
+ noSQL.removeObjects(UserData.class, query);
+ }
+
+ @Override
+ public RoleAdapter getRole(String name) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("name", name)
+ .andCondition("realmId", getOid())
+ .build();
+ RoleData role = noSQL.loadSingleObject(RoleData.class, query);
+ if (role == null) {
+ return null;
+ } else {
+ return new RoleAdapter(role, noSQL);
+ }
+ }
+
+ @Override
+ public RoleModel addRole(String name) {
+ if (getRole(name) != null) {
+ throw new IllegalArgumentException("Role " + name + " already exists");
+ }
+
+ RoleData roleData = new RoleData();
+ roleData.setName(name);
+ roleData.setRealmId(getOid());
+
+ noSQL.saveObject(roleData);
+ return new RoleAdapter(roleData, noSQL);
+ }
+
+ @Override
+ public List<RoleModel> getRoles() {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmId", getOid())
+ .build();
+ List<RoleData> roles = noSQL.loadObjects(RoleData.class, query);
+
+ List<RoleModel> result = new ArrayList<RoleModel>();
+ for (RoleData role : roles) {
+ result.add(new RoleAdapter(role, noSQL));
+ }
+
+ return result;
+ }
+
+ @Override
+ public List<RoleModel> getDefaultRoles() {
+ List<String> defaultRoles = realm.getDefaultRoles();
+
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .inCondition("_id", defaultRoles)
+ .build();
+ List<RoleData> defaultRolesData = noSQL.loadObjects(RoleData.class, query);
+
+ List<RoleModel> defaultRoleModels = new ArrayList<RoleModel>();
+ for (RoleData roleData : defaultRolesData) {
+ defaultRoleModels.add(new RoleAdapter(roleData, noSQL));
+ }
+ return defaultRoleModels;
+ }
+
+ @Override
+ public void addDefaultRole(String name) {
+ RoleModel role = getRole(name);
+ if (role == null) {
+ role = addRole(name);
+ }
+
+ noSQL.pushItemToList(realm, "defaultRoles", role.getId());
+ }
+
+ @Override
+ public void updateDefaultRoles(String[] defaultRoles) {
+ // defaultRoles is array with names of roles. So we need to convert to array of ids
+ List<String> roleIds = new ArrayList<String>();
+ for (String roleName : defaultRoles) {
+ RoleModel role = getRole(roleName);
+ if (role == null) {
+ role = addRole(roleName);
+ }
+
+ roleIds.add(role.getId());
+ }
+
+ realm.setDefaultRoles(roleIds);
+ updateRealm();
+ }
+
+ @Override
+ public ApplicationModel getApplicationById(String id) {
+ ApplicationData appData = noSQL.loadObject(ApplicationData.class, id);
+
+ // Check if application belongs to this realm
+ if (appData == null || !getOid().equals(appData.getRealmId())) {
+ return null;
+ }
+
+ ApplicationModel model = new ApplicationAdapter(appData, noSQL);
+ return model;
+ }
+
+ @Override
+ public Map<String, ApplicationModel> getApplicationNameMap() {
+ Map<String, ApplicationModel> resourceMap = new HashMap<String, ApplicationModel>();
+ for (ApplicationModel resource : getApplications()) {
+ resourceMap.put(resource.getName(), resource);
+ }
+ return resourceMap;
+ }
+
+ @Override
+ public List<ApplicationModel> getApplications() {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmId", getOid())
+ .build();
+ List<ApplicationData> appDatas = noSQL.loadObjects(ApplicationData.class, query);
+
+ List<ApplicationModel> result = new ArrayList<ApplicationModel>();
+ for (ApplicationData appData : appDatas) {
+ result.add(new ApplicationAdapter(appData, noSQL));
+ }
+ return result;
+ }
+
+ @Override
+ public ApplicationModel addApplication(String name) {
+ UserAdapter resourceUser = addUser(name);
+
+ ApplicationData appData = new ApplicationData();
+ appData.setName(name);
+ appData.setRealmId(getOid());
+ appData.setResourceUserId(resourceUser.getUser().getId());
+ noSQL.saveObject(appData);
+
+ ApplicationModel resource = new ApplicationAdapter(appData, noSQL);
+ resource.addRole("*");
+ resource.addScopeMapping(resourceUser, "*");
+ return resource;
+ }
+
+ @Override
+ public boolean hasRole(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+
+ List<String> roleIds = userData.getRoleIds();
+ String roleId = role.getId();
+ if (roleIds != null) {
+ for (String currentId : roleIds) {
+ if (roleId.equals(currentId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void grantRole(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+ noSQL.pushItemToList(userData, "roleIds", role.getId());
+ }
+
+ @Override
+ public List<RoleModel> getRoleMappings(UserModel user) {
+ List<RoleModel> result = new ArrayList<RoleModel>();
+ List<RoleData> roles = ApplicationAdapter.getAllRolesOfUser(user, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getOid().equals(role.getRealmId())) {
+ result.add(new RoleAdapter(role, noSQL));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Set<String> getRoleMappingValues(UserModel user) {
+ Set<String> result = new HashSet<String>();
+ List<RoleData> roles = ApplicationAdapter.getAllRolesOfUser(user, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getOid().equals(role.getRealmId())) {
+ result.add(role.getName());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void deleteRoleMapping(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+ noSQL.pullItemFromList(userData, "roleIds", role.getId());
+ }
+
+ @Override
+ public void addScopeMapping(UserModel agent, String roleName) {
+ RoleAdapter role = getRole(roleName);
+ if (role == null) {
+ throw new RuntimeException("Role not found");
+ }
+
+ addScopeMapping(agent, role);
+ }
+
+ @Override
+ public void addScopeMapping(UserModel agent, RoleModel role) {
+ UserData userData = ((UserAdapter)agent).getUser();
+ noSQL.pushItemToList(userData, "scopeIds", role.getId());
+ }
+
+ @Override
+ public void deleteScopeMapping(UserModel user, RoleModel role) {
+ UserData userData = ((UserAdapter)user).getUser();
+ noSQL.pullItemFromList(userData, "scopeIds", role.getId());
+ }
+
+ @Override
+ public OAuthClientModel addOAuthClient(String name) {
+ UserAdapter oauthAgent = addUser(name);
+
+ OAuthClientData oauthClient = new OAuthClientData();
+ oauthClient.setOauthAgentId(oauthAgent.getUser().getId());
+ oauthClient.setRealmId(getOid());
+ noSQL.saveObject(oauthClient);
+
+ return new OAuthClientAdapter(oauthClient, oauthAgent, noSQL);
+ }
+
+ @Override
+ public OAuthClientModel getOAuthClient(String name) {
+ UserAdapter user = getUser(name);
+ if (user == null) return null;
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmId", getOid())
+ .andCondition("oauthAgentId", user.getUser().getId())
+ .build();
+ OAuthClientData oauthClient = noSQL.loadSingleObject(OAuthClientData.class, query);
+ return oauthClient == null ? null : new OAuthClientAdapter(oauthClient, user, noSQL);
+ }
+
+ @Override
+ public List<OAuthClientModel> getOAuthClients() {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmId", getOid())
+ .build();
+ List<OAuthClientData> results = noSQL.loadObjects(OAuthClientData.class, query);
+ List<OAuthClientModel> list = new ArrayList<OAuthClientModel>();
+ for (OAuthClientData data : results) {
+ list.add(new OAuthClientAdapter(data, noSQL));
+ }
+ return list;
+ }
+
+ @Override
+ public List<RoleModel> getScopeMappings(UserModel agent) {
+ List<RoleModel> result = new ArrayList<RoleModel>();
+ List<RoleData> roles = ApplicationAdapter.getAllScopesOfUser(agent, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getOid().equals(role.getRealmId())) {
+ result.add(new RoleAdapter(role, noSQL));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public Set<String> getScopeMappingValues(UserModel agent) {
+ Set<String> result = new HashSet<String>();
+ List<RoleData> roles = ApplicationAdapter.getAllScopesOfUser(agent, noSQL);
+ // TODO: Maybe improve as currently we need to obtain all roles and then filter programmatically...
+ for (RoleData role : roles) {
+ if (getOid().equals(role.getRealmId())) {
+ result.add(role.getName());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean isRealmAdmin(UserModel agent) {
+ List<String> realmAdmins = realm.getRealmAdmins();
+ String userId = ((UserAdapter)agent).getUser().getId();
+ return realmAdmins.contains(userId);
+ }
+
+ @Override
+ public void addRealmAdmin(UserModel agent) {
+ UserData userData = ((UserAdapter)agent).getUser();
+
+ noSQL.pushItemToList(realm, "realmAdmins", userData.getId());
+ }
+
+ @Override
+ public RoleModel getRoleById(String id) {
+ RoleData role = noSQL.loadObject(RoleData.class, id);
+ if (role == null) {
+ return null;
+ } else {
+ return new RoleAdapter(role, noSQL);
+ }
+ }
+
+ @Override
+ public boolean hasRole(UserModel user, String role) {
+ RoleModel roleModel = getRole(role);
+ return hasRole(user, roleModel);
+ }
+
+ @Override
+ public void addRequiredCredential(String cred) {
+ RequiredCredentialModel credentialModel = initRequiredCredentialModel(cred);
+ addRequiredCredential(credentialModel, RequiredCredentialData.CLIENT_TYPE_USER);
+ }
+
+ @Override
+ public void addRequiredResourceCredential(String type) {
+ RequiredCredentialModel credentialModel = initRequiredCredentialModel(type);
+ addRequiredCredential(credentialModel, RequiredCredentialData.CLIENT_TYPE_RESOURCE);
+ }
+
+ @Override
+ public void addRequiredOAuthClientCredential(String type) {
+ RequiredCredentialModel credentialModel = initRequiredCredentialModel(type);
+ addRequiredCredential(credentialModel, RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE);
+ }
+
+ protected void addRequiredCredential(RequiredCredentialModel credentialModel, int clientType) {
+ RequiredCredentialData credData = new RequiredCredentialData();
+ credData.setType(credentialModel.getType());
+ credData.setFormLabel(credentialModel.getFormLabel());
+ credData.setInput(credentialModel.isInput());
+ credData.setSecret(credentialModel.isSecret());
+
+ credData.setRealmId(getOid());
+ credData.setClientType(clientType);
+
+ noSQL.saveObject(credData);
+ }
+
+ @Override
+ public void updateRequiredCredentials(Set<String> creds) {
+ List<RequiredCredentialData> credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_USER);
+ updateRequiredCredentials(creds, credsData);
+ }
+
+ @Override
+ public void updateRequiredApplicationCredentials(Set<String> creds) {
+ List<RequiredCredentialData> credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_RESOURCE);
+ updateRequiredCredentials(creds, credsData);
+ }
+
+ @Override
+ public void updateRequiredOAuthClientCredentials(Set<String> creds) {
+ List<RequiredCredentialData> credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE);
+ updateRequiredCredentials(creds, credsData);
+ }
+
+ protected void updateRequiredCredentials(Set<String> creds, List<RequiredCredentialData> credsData) {
+ Set<String> already = new HashSet<String>();
+ for (RequiredCredentialData data : credsData) {
+ if (!creds.contains(data.getType())) {
+ noSQL.removeObject(data);
+ } else {
+ already.add(data.getType());
+ }
+ }
+ for (String cred : creds) {
+ // TODO
+ System.out.println("updating cred: " + cred);
+ // logger.info("updating cred: " + cred);
+ if (!already.contains(cred)) {
+ addRequiredCredential(cred);
+ }
+ }
+ }
+
+ @Override
+ public List<RequiredCredentialModel> getRequiredCredentials() {
+ return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_USER);
+ }
+
+ @Override
+ public List<RequiredCredentialModel> getRequiredApplicationCredentials() {
+ return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_RESOURCE);
+ }
+
+ @Override
+ public List<RequiredCredentialModel> getRequiredOAuthClientCredentials() {
+ return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE);
+ }
+
+ protected List<RequiredCredentialModel> getRequiredCredentials(int credentialType) {
+ List<RequiredCredentialData> credsData = getRequiredCredentialsData(credentialType);
+
+ List<RequiredCredentialModel> result = new ArrayList<RequiredCredentialModel>();
+ for (RequiredCredentialData data : credsData) {
+ RequiredCredentialModel model = new RequiredCredentialModel();
+ model.setFormLabel(data.getFormLabel());
+ model.setInput(data.isInput());
+ model.setSecret(data.isSecret());
+ model.setType(data.getType());
+
+ result.add(model);
+ }
+ return result;
+ }
+
+ protected List<RequiredCredentialData> getRequiredCredentialsData(int credentialType) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmId", getOid())
+ .andCondition("clientType", credentialType)
+ .build();
+ return noSQL.loadObjects(RequiredCredentialData.class, query);
+ }
+
+ @Override
+ public boolean validatePassword(UserModel user, String password) {
+ Credentials.Status status = passwordCredentialHandler.validate(noSQL, ((UserAdapter)user).getUser(), password);
+ return status == Credentials.Status.VALID;
+ }
+
+ @Override
+ public boolean validateTOTP(UserModel user, String password, String token) {
+ Credentials.Status status = totpCredentialHandler.validate(noSQL, ((UserAdapter)user).getUser(), password, token, null);
+ return status == Credentials.Status.VALID;
+ }
+
+ @Override
+ public void updateCredential(UserModel user, UserCredentialModel cred) {
+ if (cred.getType().equals(CredentialRepresentation.PASSWORD)) {
+ passwordCredentialHandler.update(noSQL, ((UserAdapter)user).getUser(), cred.getValue(), null, null);
+ } else if (cred.getType().equals(CredentialRepresentation.TOTP)) {
+ totpCredentialHandler.update(noSQL, ((UserAdapter)user).getUser(), cred.getValue(), cred.getDevice(), null, null);
+ } else if (cred.getType().equals(CredentialRepresentation.CLIENT_CERT)) {
+ // TODO
+// X509Certificate cert = null;
+// try {
+// cert = org.keycloak.PemUtils.decodeCertificate(cred.getValue());
+// } catch (Exception e) {
+// throw new RuntimeException(e);
+// }
+// X509CertificateCredentials creds = new X509CertificateCredentials(cert);
+// idm.updateCredential(((UserAdapter)user).getUser(), creds);
+ }
+ }
+
+ @Override
+ public UserModel getUserBySocialLink(SocialLinkModel socialLink) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("socialProvider", socialLink.getSocialProvider())
+ .andCondition("socialUsername", socialLink.getSocialUsername())
+ .andCondition("realmId", getOid())
+ .build();
+ SocialLinkData socialLinkData = noSQL.loadSingleObject(SocialLinkData.class, query);
+
+ if (socialLinkData == null) {
+ return null;
+ } else {
+ UserData userData = noSQL.loadObject(UserData.class, socialLinkData.getUserId());
+ // TODO: Add some checking if userData exists and programmatically remove binding if it doesn't? (There are more similar places where this should be handled)
+ return new UserAdapter(userData, noSQL);
+ }
+ }
+
+ @Override
+ public Set<SocialLinkModel> getSocialLinks(UserModel user) {
+ UserData userData = ((UserAdapter)user).getUser();
+ String userId = userData.getId();
+
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", userId)
+ .build();
+ List<SocialLinkData> dbSocialLinks = noSQL.loadObjects(SocialLinkData.class, query);
+
+ Set<SocialLinkModel> result = new HashSet<SocialLinkModel>();
+ for (SocialLinkData socialLinkData : dbSocialLinks) {
+ SocialLinkModel model = new SocialLinkModel(socialLinkData.getSocialProvider(), socialLinkData.getSocialUsername());
+ result.add(model);
+ }
+ return result;
+ }
+
+ @Override
+ public void addSocialLink(UserModel user, SocialLinkModel socialLink) {
+ UserData userData = ((UserAdapter)user).getUser();
+ SocialLinkData socialLinkData = new SocialLinkData();
+ socialLinkData.setSocialProvider(socialLink.getSocialProvider());
+ socialLinkData.setSocialUsername(socialLink.getSocialUsername());
+ socialLinkData.setUserId(userData.getId());
+ socialLinkData.setRealmId(getOid());
+
+ noSQL.saveObject(socialLinkData);
+ }
+
+ @Override
+ public void removeSocialLink(UserModel user, SocialLinkModel socialLink) {
+ UserData userData = ((UserAdapter)user).getUser();
+ String userId = userData.getId();
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("socialProvider", socialLink.getSocialProvider())
+ .andCondition("socialUsername", socialLink.getSocialUsername())
+ .andCondition("userId", userId)
+ .build();
+ noSQL.removeObjects(SocialLinkData.class, query);
+ }
+
+ protected void updateRealm() {
+ noSQL.saveObject(realm);
+ }
+
+ protected RequiredCredentialModel initRequiredCredentialModel(String type) {
+ RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type);
+ if (model == null) {
+ throw new RuntimeException("Unknown credential type " + type);
+ }
+ return model;
+ }
+
+ @Override
+ public List<UserModel> searchForUserByAttributes(Map<String, String> attributes) {
+ NoSQLQueryBuilder queryBuilder = noSQL.createQueryBuilder();
+ for (Map.Entry<String, String> entry : attributes.entrySet()) {
+ if (entry.getKey().equals(UserModel.LOGIN_NAME)) {
+ queryBuilder.andCondition("loginName", entry.getValue());
+ } else if (entry.getKey().equalsIgnoreCase(UserModel.FIRST_NAME)) {
+ queryBuilder.andCondition(UserModel.FIRST_NAME, entry.getValue());
+
+ } else if (entry.getKey().equalsIgnoreCase(UserModel.LAST_NAME)) {
+ queryBuilder.andCondition(UserModel.LAST_NAME, entry.getValue());
+
+ } else if (entry.getKey().equalsIgnoreCase(UserModel.EMAIL)) {
+ queryBuilder.andCondition(UserModel.EMAIL, entry.getValue());
+ }
+ }
+ List<UserData> users = noSQL.loadObjects(UserData.class, queryBuilder.build());
+ List<UserModel> userModels = new ArrayList<UserModel>();
+ for (UserData user : users) {
+ userModels.add(new UserAdapter(user, noSQL));
+ }
+ return userModels;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
new file mode 100644
index 0000000..7b2692f
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RoleAdapter.java
@@ -0,0 +1,52 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.keycloak.data.RoleData;
+
+/**
+ * Wrapper around RoleData object, which will persist wrapped object after each set operation (compatibility with picketlink based impl)
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleAdapter implements RoleModel {
+
+ private final RoleData role;
+ private final NoSQL noSQL;
+
+ public RoleAdapter(RoleData roleData, NoSQL noSQL) {
+ this.role = roleData;
+ this.noSQL = noSQL;
+ }
+
+ @Override
+ public String getName() {
+ return role.getName();
+ }
+
+ @Override
+ public String getDescription() {
+ return role.getDescription();
+ }
+
+ @Override
+ public void setDescription(String description) {
+ role.setDescription(description);
+ noSQL.saveObject(role);
+ }
+
+ @Override
+ public String getId() {
+ return role.getId();
+ }
+
+ @Override
+ public void setName(String name) {
+ role.setName(name);
+ noSQL.saveObject(role);
+ }
+
+ public RoleData getRole() {
+ return role;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
new file mode 100644
index 0000000..c047361
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -0,0 +1,152 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.models.UserModel;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+
+/**
+ * Wrapper around UserData object, which will persist wrapped object after each set operation (compatibility with picketlink based impl)
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserAdapter implements UserModel {
+
+ private final UserData user;
+ private final NoSQL noSQL;
+
+ public UserAdapter(UserData userData, NoSQL noSQL) {
+ this.user = userData;
+ this.noSQL = noSQL;
+ }
+
+ @Override
+ public String getLoginName() {
+ return user.getLoginName();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return user.isEnabled();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ user.setEnabled(enabled);
+ noSQL.saveObject(user);
+ }
+
+ @Override
+ public String getFirstName() {
+ return user.getFirstName();
+ }
+
+ @Override
+ public void setFirstName(String firstName) {
+ user.setFirstName(firstName);
+ noSQL.saveObject(user);
+ }
+
+ @Override
+ public String getLastName() {
+ return user.getLastName();
+ }
+
+ @Override
+ public void setLastName(String lastName) {
+ user.setLastName(lastName);
+ noSQL.saveObject(user);
+ }
+
+ @Override
+ public String getEmail() {
+ return user.getEmail();
+ }
+
+ @Override
+ public void setEmail(String email) {
+ user.setEmail(email);
+ noSQL.saveObject(user);
+ }
+
+ @Override
+ public boolean isEmailVerified() {
+ return user.isEmailVerified();
+ }
+
+ @Override
+ public void setEmailVerified(boolean verified) {
+ user.setEmailVerified(verified);
+ noSQL.saveObject(user);
+ }
+
+ @Override
+ public void setAttribute(String name, String value) {
+ user.setAttribute(name, value);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ user.removeAttribute(name);
+ noSQL.saveObject(user);
+ }
+
+ @Override
+ public String getAttribute(String name) {
+ return user.getAttribute(name);
+ }
+
+ @Override
+ public Map<String, String> getAttributes() {
+ return user.getAttributes();
+ }
+
+ public UserData getUser() {
+ return user;
+ }
+
+ @Override
+ public Set<RequiredAction> getRequiredActions() {
+ List<RequiredAction> actions = user.getRequiredActions();
+
+ // Compatibility with picketlink impl
+ if (actions == null) {
+ return Collections.emptySet();
+ } else {
+ Set<RequiredAction> s = new HashSet<RequiredAction>();
+ for (RequiredAction a : actions) {
+ s.add(a);
+ }
+ return Collections.unmodifiableSet(s);
+ }
+ }
+
+ @Override
+ public void addRequiredAction(RequiredAction action) {
+ // Push action only if it's not already here
+ if (user.getRequiredActions() == null || !user.getRequiredActions().contains(action)) {
+ noSQL.pushItemToList(user, "requiredActions", action);
+ }
+ }
+
+ @Override
+ public void removeRequiredAction(RequiredAction action) {
+ noSQL.pullItemFromList(user, "requiredActions", action);
+ }
+
+ @Override
+ public boolean isTotp() {
+ return user.isTotp();
+ }
+
+ @Override
+ public void setTotp(boolean totp) {
+ user.setTotp(totp);
+ noSQL.saveObject(user);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/PasswordCredentialHandler.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/PasswordCredentialHandler.java
new file mode 100644
index 0000000..719760a
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/PasswordCredentialHandler.java
@@ -0,0 +1,154 @@
+package org.keycloak.models.mongo.keycloak.credentials;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.UUID;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+import org.keycloak.models.mongo.keycloak.data.credentials.PasswordData;
+import org.picketlink.idm.credential.Credentials;
+import org.picketlink.idm.credential.encoder.PasswordEncoder;
+import org.picketlink.idm.credential.encoder.SHAPasswordEncoder;
+
+/**
+ * Defacto forked from {@link org.picketlink.idm.credential.handler.PasswordCredentialHandler}
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PasswordCredentialHandler {
+
+ private static final String DEFAULT_SALT_ALGORITHM = "SHA1PRNG";
+
+ /**
+ * <p>
+ * Stores a <b>stateless</b> instance of {@link org.picketlink.idm.credential.encoder.PasswordEncoder} that should be used to encode passwords.
+ * </p>
+ */
+ public static final String PASSWORD_ENCODER = "PASSWORD_ENCODER";
+
+ private PasswordEncoder passwordEncoder = new SHAPasswordEncoder(512);;
+
+ public PasswordCredentialHandler(Map<String, Object> options) {
+ setup(options);
+ }
+
+ private void setup(Map<String, Object> options) {
+ if (options != null) {
+ Object providedEncoder = options.get(PASSWORD_ENCODER);
+
+ if (providedEncoder != null) {
+ if (PasswordEncoder.class.isInstance(providedEncoder)) {
+ this.passwordEncoder = (PasswordEncoder) providedEncoder;
+ } else {
+ throw new IllegalArgumentException("The password encoder [" + providedEncoder
+ + "] must be an instance of " + PasswordEncoder.class.getName());
+ }
+ }
+ }
+ }
+
+ public Credentials.Status validate(NoSQL noSQL, UserData user, String passwordToValidate) {
+ Credentials.Status status = Credentials.Status.INVALID;
+
+ user = noSQL.loadObject(UserData.class, user.getId());
+
+ // If the user for the provided username cannot be found we fail validation
+ if (user != null) {
+ if (user.isEnabled()) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .build();
+ PasswordData passwordData = noSQL.loadSingleObject(PasswordData.class, query);
+
+ // If the stored hash is null we automatically fail validation
+ if (passwordData != null) {
+ // TODO: Status.INVALID should have bigger priority than Status.EXPIRED?
+ if (!isCredentialExpired(passwordData.getExpiryDate())) {
+
+ boolean matches = this.passwordEncoder.verify(saltPassword(passwordToValidate, passwordData.getSalt()), passwordData.getEncodedHash());
+
+ if (matches) {
+ status = Credentials.Status.VALID;
+ }
+ } else {
+ status = Credentials.Status.EXPIRED;
+ }
+ }
+ } else {
+ status = Credentials.Status.ACCOUNT_DISABLED;
+ }
+ }
+
+ return status;
+ }
+
+ public void update(NoSQL noSQL, UserData user, String password,
+ Date effectiveDate, Date expiryDate) {
+
+ // Delete existing password of user
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .build();
+ noSQL.removeObjects(PasswordData.class, query);
+
+ PasswordData passwordData = new PasswordData();
+
+ String passwordSalt = generateSalt();
+
+ passwordData.setSalt(passwordSalt);
+ passwordData.setEncodedHash(this.passwordEncoder.encode(saltPassword(password, passwordSalt)));
+
+ if (effectiveDate != null) {
+ passwordData.setEffectiveDate(effectiveDate);
+ }
+
+ passwordData.setExpiryDate(expiryDate);
+
+ passwordData.setUserId(user.getId());
+
+ noSQL.saveObject(passwordData);
+ }
+
+ /**
+ * <p>
+ * Salt the give <code>rawPassword</code> with the specified <code>salt</code> value.
+ * </p>
+ *
+ * @param rawPassword
+ * @param salt
+ * @return
+ */
+ private String saltPassword(String rawPassword, String salt) {
+ return salt + rawPassword;
+ }
+
+ /**
+ * <p>
+ * Generates a random string to be used as a salt for passwords.
+ * </p>
+ *
+ * @return
+ */
+ private String generateSalt() {
+ // TODO: always returns same salt (See https://issues.jboss.org/browse/PLINK-258)
+ /*SecureRandom pseudoRandom = null;
+
+ try {
+ pseudoRandom = SecureRandom.getInstance(DEFAULT_SALT_ALGORITHM);
+ pseudoRandom.setSeed(1024);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Error getting SecureRandom instance: " + DEFAULT_SALT_ALGORITHM, e);
+ }
+
+ return String.valueOf(pseudoRandom.nextLong());*/
+ return UUID.randomUUID().toString();
+ }
+
+ public static boolean isCredentialExpired(Date expiryDate) {
+ return expiryDate != null && new Date().compareTo(expiryDate) > 0;
+ }
+
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java
new file mode 100644
index 0000000..b8f02e7
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java
@@ -0,0 +1,138 @@
+package org.keycloak.models.mongo.keycloak.credentials;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.data.UserData;
+import org.keycloak.models.mongo.keycloak.data.credentials.OTPData;
+import org.picketlink.idm.credential.Credentials;
+import org.picketlink.idm.credential.util.TimeBasedOTP;
+
+import static org.picketlink.common.util.StringUtil.isNullOrEmpty;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_ALGORITHM;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_DELAY_WINDOW;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_INTERVAL_SECONDS;
+import static org.picketlink.idm.credential.util.TimeBasedOTP.DEFAULT_NUMBER_DIGITS;
+
+/**
+ * Defacto forked from {@link org.picketlink.idm.credential.handler.TOTPCredentialHandler}
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TOTPCredentialHandler extends PasswordCredentialHandler {
+
+ public static final String ALGORITHM = "ALGORITHM";
+ public static final String INTERVAL_SECONDS = "INTERVAL_SECONDS";
+ public static final String NUMBER_DIGITS = "NUMBER_DIGITS";
+ public static final String DELAY_WINDOW = "DELAY_WINDOW";
+ public static final String DEFAULT_DEVICE = "DEFAULT_DEVICE";
+
+ private TimeBasedOTP totp;
+
+ public TOTPCredentialHandler(Map<String, Object> options) {
+ super(options);
+ setup(options);
+ }
+
+ private void setup(Map<String, Object> options) {
+ String algorithm = getConfigurationProperty(options, ALGORITHM, DEFAULT_ALGORITHM);
+ String intervalSeconds = getConfigurationProperty(options, INTERVAL_SECONDS, "" + DEFAULT_INTERVAL_SECONDS);
+ String numberDigits = getConfigurationProperty(options, NUMBER_DIGITS, "" + DEFAULT_NUMBER_DIGITS);
+ String delayWindow = getConfigurationProperty(options, DELAY_WINDOW, "" + DEFAULT_DELAY_WINDOW);
+
+ this.totp = new TimeBasedOTP(algorithm, Integer.parseInt(numberDigits), Integer.valueOf(intervalSeconds), Integer.valueOf(delayWindow));
+ }
+
+ public Credentials.Status validate(NoSQL noSQL, UserData user, String passwordToValidate, String token, String device) {
+ Credentials.Status status = super.validate(noSQL, user, passwordToValidate);
+
+ if (Credentials.Status.VALID != status) {
+ return status;
+ }
+
+ device = getDevice(device);
+
+ user = noSQL.loadObject(UserData.class, user.getId());
+
+ // If the user for the provided username cannot be found we fail validation
+ if (user != null) {
+ if (user.isEnabled()) {
+
+ // Try to find OTP based on userId and device (For now assume that this is unique combination)
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .andCondition("device", device)
+ .build();
+ OTPData otpData = noSQL.loadSingleObject(OTPData.class, query);
+
+ // If the stored OTP is null we automatically fail validation
+ if (otpData != null) {
+ // TODO: Status.INVALID should have bigger priority than Status.EXPIRED?
+ if (!PasswordCredentialHandler.isCredentialExpired(otpData.getExpiryDate())) {
+ boolean isValid = this.totp.validate(token, otpData.getSecretKey().getBytes());
+ if (!isValid) {
+ status = Credentials.Status.INVALID;
+ }
+ } else {
+ status = Credentials.Status.EXPIRED;
+ }
+ } else {
+ status = Credentials.Status.UNVALIDATED;
+ }
+ } else {
+ status = Credentials.Status.ACCOUNT_DISABLED;
+ }
+ } else {
+ status = Credentials.Status.INVALID;
+ }
+
+ return status;
+ }
+
+ public void update(NoSQL noSQL, UserData user, String secret, String device, Date effectiveDate, Date expiryDate) {
+ device = getDevice(device);
+
+ // Try to look if user already has otp (Right now, supports just one OTP per user)
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", user.getId())
+ .andCondition("device", device)
+ .build();
+
+ OTPData otpData = noSQL.loadSingleObject(OTPData.class, query);
+ if (otpData == null) {
+ otpData = new OTPData();
+ }
+
+ otpData.setSecretKey(secret);
+ otpData.setDevice(device);
+
+ if (effectiveDate != null) {
+ otpData.setEffectiveDate(effectiveDate);
+ }
+
+ otpData.setExpiryDate(expiryDate);
+ otpData.setUserId(user.getId());
+
+ noSQL.saveObject(otpData);
+ }
+
+ private String getDevice(String device) {
+ if (isNullOrEmpty(device)) {
+ device = DEFAULT_DEVICE;
+ }
+
+ return device;
+ }
+
+ private String getConfigurationProperty(Map<String, Object> options, String key, String defaultValue) {
+ Object value = options.get(key);
+
+ if (value != null) {
+ return String.valueOf(value);
+ }
+
+ return defaultValue;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java
new file mode 100644
index 0000000..5ceb788
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java
@@ -0,0 +1,109 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "applications")
+public class ApplicationData implements NoSQLObject {
+
+ private String id;
+ private String name;
+ private boolean enabled;
+ private boolean surrogateAuthRequired;
+ private String managementUrl;
+ private String baseUrl;
+
+ private String resourceUserId;
+ private String realmId;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @NoSQLField
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NoSQLField
+ public boolean isSurrogateAuthRequired() {
+ return surrogateAuthRequired;
+ }
+
+ public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
+ this.surrogateAuthRequired = surrogateAuthRequired;
+ }
+
+ @NoSQLField
+ public String getManagementUrl() {
+ return managementUrl;
+ }
+
+ public void setManagementUrl(String managementUrl) {
+ this.managementUrl = managementUrl;
+ }
+
+ @NoSQLField
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ @NoSQLField
+ public String getResourceUserId() {
+ return resourceUserId;
+ }
+
+ public void setResourceUserId(String resourceUserId) {
+ this.resourceUserId = resourceUserId;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ // Remove resourceUser of this application
+ noSQL.removeObject(UserData.class, resourceUserId);
+
+ // Remove all roles, which belongs to this application
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("applicationId", id)
+ .build();
+ noSQL.removeObjects(RoleData.class, query);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java
new file mode 100644
index 0000000..8ab31a6
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java
@@ -0,0 +1,66 @@
+package org.keycloak.models.mongo.keycloak.data.credentials;
+
+import java.util.Date;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "otpCredentials")
+public class OTPData extends AbstractNoSQLObject {
+
+ private Date effectiveDate = new Date();
+ private Date expiryDate;
+ private String secretKey;
+ private String device;
+
+ private String userId;
+
+ @NoSQLField
+ public Date getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public void setEffectiveDate(Date effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ @NoSQLField
+ public Date getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(Date expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+ @NoSQLField
+ public String getSecretKey() {
+ return secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ @NoSQLField
+ public String getDevice() {
+ return device;
+ }
+
+ public void setDevice(String device) {
+ this.device = device;
+ }
+
+ @NoSQLField
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java
new file mode 100644
index 0000000..7480e1f
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java
@@ -0,0 +1,66 @@
+package org.keycloak.models.mongo.keycloak.data.credentials;
+
+import java.util.Date;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "passwordCredentials")
+public class PasswordData extends AbstractNoSQLObject {
+
+ private Date effectiveDate = new Date();
+ private Date expiryDate;
+ private String encodedHash;
+ private String salt;
+
+ private String userId;
+
+ @NoSQLField
+ public Date getEffectiveDate() {
+ return effectiveDate;
+ }
+
+ public void setEffectiveDate(Date effectiveDate) {
+ this.effectiveDate = effectiveDate;
+ }
+
+ @NoSQLField
+ public Date getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(Date expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+ @NoSQLField
+ public String getEncodedHash() {
+ return encodedHash;
+ }
+
+ public void setEncodedHash(String encodedHash) {
+ this.encodedHash = encodedHash;
+ }
+
+ @NoSQLField
+ public String getSalt() {
+ return salt;
+ }
+
+ public void setSalt(String salt) {
+ this.salt = salt;
+ }
+
+ @NoSQLField
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java
new file mode 100644
index 0000000..67f74ee
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java
@@ -0,0 +1,62 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "oauthClients")
+public class OAuthClientData implements NoSQLObject {
+
+ private String id;
+ private String baseUrl;
+
+ private String oauthAgentId;
+ private String realmId;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ @NoSQLField
+ public String getOauthAgentId() {
+ return oauthAgentId;
+ }
+
+ public void setOauthAgentId(String oauthUserId) {
+ this.oauthAgentId = oauthUserId;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ // Remove user of this oauthClient
+ noSQL.removeObject(UserData.class, oauthAgentId);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RealmData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RealmData.java
new file mode 100644
index 0000000..5247d60
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RealmData.java
@@ -0,0 +1,219 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import java.util.List;
+
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "realms")
+public class RealmData implements NoSQLObject {
+
+ private String oid;
+
+ private String id;
+ private String name;
+ private boolean enabled;
+ private boolean sslNotRequired;
+ private boolean cookieLoginAllowed;
+ private boolean registrationAllowed;
+ private boolean verifyEmail;
+ private boolean resetPasswordAllowed;
+ private boolean social;
+ private boolean automaticRegistrationAfterSocialLogin;
+ private int tokenLifespan;
+ private int accessCodeLifespan;
+ private int accessCodeLifespanUserAction;
+ private String publicKeyPem;
+ private String privateKeyPem;
+
+ private List<String> defaultRoles;
+ private List<String> realmAdmins;
+
+ @NoSQLId
+ public String getOid() {
+ return oid;
+ }
+
+ public void setOid(String oid) {
+ this.oid = oid;
+ }
+
+ @NoSQLField
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String realmName) {
+ this.name = realmName;
+ }
+
+ @NoSQLField
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NoSQLField
+ public boolean isSslNotRequired() {
+ return sslNotRequired;
+ }
+
+ public void setSslNotRequired(boolean sslNotRequired) {
+ this.sslNotRequired = sslNotRequired;
+ }
+
+ @NoSQLField
+ public boolean isCookieLoginAllowed() {
+ return cookieLoginAllowed;
+ }
+
+ public void setCookieLoginAllowed(boolean cookieLoginAllowed) {
+ this.cookieLoginAllowed = cookieLoginAllowed;
+ }
+
+ @NoSQLField
+ public boolean isRegistrationAllowed() {
+ return registrationAllowed;
+ }
+
+ public void setRegistrationAllowed(boolean registrationAllowed) {
+ this.registrationAllowed = registrationAllowed;
+ }
+
+ @NoSQLField
+ public boolean isVerifyEmail() {
+ return verifyEmail;
+ }
+
+ public void setVerifyEmail(boolean verifyEmail) {
+ this.verifyEmail = verifyEmail;
+ }
+
+ @NoSQLField
+ public boolean isResetPasswordAllowed() {
+ return resetPasswordAllowed;
+ }
+
+ public void setResetPasswordAllowed(boolean resetPasswordAllowed) {
+ this.resetPasswordAllowed = resetPasswordAllowed;
+ }
+
+ @NoSQLField
+ public boolean isSocial() {
+ return social;
+ }
+
+ public void setSocial(boolean social) {
+ this.social = social;
+ }
+
+ @NoSQLField
+ public boolean isAutomaticRegistrationAfterSocialLogin() {
+ return automaticRegistrationAfterSocialLogin;
+ }
+
+ public void setAutomaticRegistrationAfterSocialLogin(boolean automaticRegistrationAfterSocialLogin) {
+ this.automaticRegistrationAfterSocialLogin = automaticRegistrationAfterSocialLogin;
+ }
+
+ @NoSQLField
+ public int getTokenLifespan() {
+ return tokenLifespan;
+ }
+
+ public void setTokenLifespan(int tokenLifespan) {
+ this.tokenLifespan = tokenLifespan;
+ }
+
+ @NoSQLField
+ public int getAccessCodeLifespan() {
+ return accessCodeLifespan;
+ }
+
+ public void setAccessCodeLifespan(int accessCodeLifespan) {
+ this.accessCodeLifespan = accessCodeLifespan;
+ }
+
+ @NoSQLField
+ public int getAccessCodeLifespanUserAction() {
+ return accessCodeLifespanUserAction;
+ }
+
+ public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) {
+ this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
+ }
+
+ @NoSQLField
+ public String getPublicKeyPem() {
+ return publicKeyPem;
+ }
+
+ public void setPublicKeyPem(String publicKeyPem) {
+ this.publicKeyPem = publicKeyPem;
+ }
+
+ @NoSQLField
+ public String getPrivateKeyPem() {
+ return privateKeyPem;
+ }
+
+ public void setPrivateKeyPem(String privateKeyPem) {
+ this.privateKeyPem = privateKeyPem;
+ }
+
+ @NoSQLField
+ public List<String> getDefaultRoles() {
+ return defaultRoles;
+ }
+
+ public void setDefaultRoles(List<String> defaultRoles) {
+ this.defaultRoles = defaultRoles;
+ }
+
+ @NoSQLField
+ public List<String> getRealmAdmins() {
+ return realmAdmins;
+ }
+
+ public void setRealmAdmins(List<String> realmAdmins) {
+ this.realmAdmins = realmAdmins;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("realmId", oid)
+ .build();
+
+ // Remove all users of this realm
+ noSQL.removeObjects(UserData.class, query);
+
+ // Remove all requiredCredentials of this realm
+ noSQL.removeObjects(RequiredCredentialData.class, query);
+
+ // Remove all roles of this realm
+ noSQL.removeObjects(RoleData.class, query);
+
+ // Remove all applications of this realm
+ noSQL.removeObjects(ApplicationData.class, query);
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java
new file mode 100644
index 0000000..e46ee9f
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java
@@ -0,0 +1,90 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "requiredCredentials")
+public class RequiredCredentialData extends AbstractNoSQLObject {
+
+ public static final int CLIENT_TYPE_USER = 1;
+ public static final int CLIENT_TYPE_RESOURCE = 2;
+ public static final int CLIENT_TYPE_OAUTH_RESOURCE = 3;
+
+ private String id;
+
+ private String type;
+ private boolean input;
+ private boolean secret;
+ private String formLabel;
+
+ private String realmId;
+ private int clientType;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @NoSQLField
+ public boolean isInput() {
+ return input;
+ }
+
+ public void setInput(boolean input) {
+ this.input = input;
+ }
+
+ @NoSQLField
+ public boolean isSecret() {
+ return secret;
+ }
+
+ public void setSecret(boolean secret) {
+ this.secret = secret;
+ }
+
+ @NoSQLField
+ public String getFormLabel() {
+ return formLabel;
+ }
+
+ public void setFormLabel(String formLabel) {
+ this.formLabel = formLabel;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @NoSQLField
+ public int getClientType() {
+ return clientType;
+ }
+
+ public void setClientType(int clientType) {
+ this.clientType = clientType;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RoleData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RoleData.java
new file mode 100644
index 0000000..29bc1f8
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RoleData.java
@@ -0,0 +1,98 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import java.util.List;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "roles")
+public class RoleData implements NoSQLObject {
+
+ private static final Logger logger = Logger.getLogger(RoleData.class);
+
+ private String id;
+ private String name;
+ private String description;
+
+ private String realmId;
+ private String applicationId;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @NoSQLField
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @NoSQLField
+ public String getApplicationId() {
+ return applicationId;
+ }
+
+ public void setApplicationId(String applicationId) {
+ this.applicationId = applicationId;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ // Remove this role from all users, which has it
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("roleIds", id)
+ .build();
+
+ List<UserData> users = noSQL.loadObjects(UserData.class, query);
+ for (UserData user : users) {
+ logger.info("Removing role " + getName() + " from user " + user.getLoginName());
+ noSQL.pullItemFromList(user, "roleIds", getId());
+ }
+
+ // Remove this scope from all users, which has it
+ query = noSQL.createQueryBuilder()
+ .andCondition("scopeIds", id)
+ .build();
+
+ users = noSQL.loadObjects(UserData.class, query);
+ for (UserData user : users) {
+ logger.info("Removing scope " + getName() + " from user " + user.getLoginName());
+ noSQL.pullItemFromList(user, "scopeIds", getId());
+ }
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/SocialLinkData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/SocialLinkData.java
new file mode 100644
index 0000000..37ea43d
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/SocialLinkData.java
@@ -0,0 +1,55 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "socialLinks")
+public class SocialLinkData extends AbstractNoSQLObject {
+
+ private String socialUsername;
+ private String socialProvider;
+ private String userId;
+ // realmId is needed to allow searching as combination socialUsername+socialProvider may not be unique
+ // (Same user could have mapped same facebook account to username "foo" in "realm1" and to username "bar" in "realm2")
+ private String realmId;
+
+ @NoSQLField
+ public String getSocialUsername() {
+ return socialUsername;
+ }
+
+ public void setSocialUsername(String socialUsername) {
+ this.socialUsername = socialUsername;
+ }
+
+ @NoSQLField
+ public String getSocialProvider() {
+ return socialProvider;
+ }
+
+ public void setSocialProvider(String socialProvider) {
+ this.socialProvider = socialProvider;
+ }
+
+ @NoSQLField
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/UserData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/UserData.java
new file mode 100644
index 0000000..cfeb67d
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/UserData.java
@@ -0,0 +1,167 @@
+package org.keycloak.models.mongo.keycloak.data;
+
+import java.util.List;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.mongo.api.AbstractAttributedNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.keycloak.data.credentials.PasswordData;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "users")
+public class UserData extends AbstractAttributedNoSQLObject {
+
+ private static final Logger logger = Logger.getLogger(UserData.class);
+
+ private String id;
+ private String loginName;
+ private String firstName;
+ private String lastName;
+ private String email;
+ private boolean emailVerified;
+ private boolean totp;
+ private boolean enabled;
+
+ private String realmId;
+
+ private List<String> roleIds;
+ private List<String> scopeIds;
+ private List<UserModel.RequiredAction> requiredActions;
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getLoginName() {
+ return loginName;
+ }
+
+ public void setLoginName(String loginName) {
+ this.loginName = loginName;
+ }
+
+ @NoSQLField
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ @NoSQLField
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @NoSQLField
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ @NoSQLField
+ public boolean isEmailVerified() {
+ return emailVerified;
+ }
+
+ public void setEmailVerified(boolean emailVerified) {
+ this.emailVerified = emailVerified;
+ }
+
+ @NoSQLField
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NoSQLField
+ public boolean isTotp() {
+ return totp;
+ }
+
+ public void setTotp(boolean totp) {
+ this.totp = totp;
+ }
+
+ @NoSQLField
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ @NoSQLField
+ public List<String> getRoleIds() {
+ return roleIds;
+ }
+
+ public void setRoleIds(List<String> roleIds) {
+ this.roleIds = roleIds;
+ }
+
+ @NoSQLField
+ public List<String> getScopeIds() {
+ return scopeIds;
+ }
+
+ public void setScopeIds(List<String> scopeIds) {
+ this.scopeIds = scopeIds;
+ }
+
+ @NoSQLField
+ public List<UserModel.RequiredAction> getRequiredActions() {
+ return requiredActions;
+ }
+
+ public void setRequiredActions(List<UserModel.RequiredAction> requiredActions) {
+ this.requiredActions = requiredActions;
+ }
+
+ @Override
+ public void afterRemove(NoSQL noSQL) {
+ NoSQLQuery query = noSQL.createQueryBuilder()
+ .andCondition("userId", id)
+ .build();
+
+ // Remove social links and passwords of this user
+ noSQL.removeObjects(SocialLinkData.class, query);
+ noSQL.removeObjects(PasswordData.class, query);
+
+ // Remove this user from all realms, which have him as an admin
+ NoSQLQuery realmQuery = noSQL.createQueryBuilder()
+ .andCondition("realmAdmins", id)
+ .build();
+
+ List<RealmData> realms = noSQL.loadObjects(RealmData.class, realmQuery);
+ for (RealmData realm : realms) {
+ logger.info("Removing admin user " + getLoginName() + " from realm " + realm.getId());
+ noSQL.pullItemFromList(realm, "realmAdmins", getId());
+ }
+ }
+}
diff --git a/model/mongo/src/test/java/org/keycloak/models/mongo/test/Address.java b/model/mongo/src/test/java/org/keycloak/models/mongo/test/Address.java
new file mode 100644
index 0000000..8f6b6f8
--- /dev/null
+++ b/model/mongo/src/test/java/org/keycloak/models/mongo/test/Address.java
@@ -0,0 +1,43 @@
+package org.keycloak.models.mongo.test;
+
+import java.util.List;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLField;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class Address extends AbstractNoSQLObject {
+
+ private String street;
+ private int number;
+ private List<String> flatNumbers;
+
+ @NoSQLField
+ public String getStreet() {
+ return street;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ @NoSQLField
+ public int getNumber() {
+ return number;
+ }
+
+ public void setNumber(int number) {
+ this.number = number;
+ }
+
+ @NoSQLField
+ public List<String> getFlatNumbers() {
+ return flatNumbers;
+ }
+
+ public void setFlatNumbers(List<String> flatNumbers) {
+ this.flatNumbers = flatNumbers;
+ }
+}
diff --git a/model/mongo/src/test/java/org/keycloak/models/mongo/test/MongoDBModelTest.java b/model/mongo/src/test/java/org/keycloak/models/mongo/test/MongoDBModelTest.java
new file mode 100644
index 0000000..262ade8
--- /dev/null
+++ b/model/mongo/src/test/java/org/keycloak/models/mongo/test/MongoDBModelTest.java
@@ -0,0 +1,111 @@
+package org.keycloak.models.mongo.test;
+
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.mongodb.DB;
+import com.mongodb.MongoClient;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.keycloak.models.mongo.api.NoSQL;
+import org.keycloak.models.mongo.api.NoSQLObject;
+import org.keycloak.models.mongo.api.query.NoSQLQuery;
+import org.keycloak.models.mongo.impl.MongoDBImpl;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MongoDBModelTest {
+
+ private static final Class<? extends NoSQLObject>[] MANAGED_DATA_TYPES = (Class<? extends NoSQLObject>[])new Class<?>[] {
+ Person.class,
+ Address.class,
+ };
+
+ private MongoClient mongoClient;
+ private NoSQL mongoDB;
+
+ @Before
+ public void before() throws Exception {
+ try {
+ // TODO: authentication support
+ mongoClient = new MongoClient("localhost", 27017);
+
+ DB db = mongoClient.getDB("keycloakTest");
+ mongoDB = new MongoDBImpl(db, true, MANAGED_DATA_TYPES);
+
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @After
+ public void after() throws Exception {
+ mongoClient.close();
+ }
+
+ // @Test
+ public void mongoModelTest() throws Exception {
+ // Add some user
+ Person john = new Person();
+ john.setFirstName("john");
+ john.setAge(25);
+ john.setGender(Person.Gender.MALE);
+
+ mongoDB.saveObject(john);
+
+ // Add another user
+ Person mary = new Person();
+ mary.setFirstName("mary");
+ mary.setKids(Arrays.asList(new String[] {"Peter", "Paul", "Wendy"}));
+
+ Address addr1 = new Address();
+ addr1.setStreet("Elm");
+ addr1.setNumber(5);
+ addr1.setFlatNumbers(Arrays.asList(new String[] {"flat1", "flat2"}));
+ Address addr2 = new Address();
+ List<Address> addresses = new ArrayList<Address>();
+ addresses.add(addr1);
+ addresses.add(addr2);
+
+ mary.setAddresses(addresses);
+ mary.setMainAddress(addr1);
+ mary.setGender(Person.Gender.FEMALE);
+ mary.setGenders(Arrays.asList(new Person.Gender[] {Person.Gender.FEMALE}));
+ mongoDB.saveObject(mary);
+
+ Assert.assertEquals(2, mongoDB.loadObjects(Person.class, mongoDB.createQueryBuilder().build()).size());
+
+ NoSQLQuery query = mongoDB.createQueryBuilder().andCondition("addresses.flatNumbers", "flat1").build();
+ List<Person> persons = mongoDB.loadObjects(Person.class, query);
+ Assert.assertEquals(1, persons.size());
+ mary = persons.get(0);
+ Assert.assertEquals(mary.getFirstName(), "mary");
+ Assert.assertTrue(mary.getKids().contains("Paul"));
+ Assert.assertEquals(2, mary.getAddresses().size());
+ Assert.assertEquals(Address.class, mary.getAddresses().get(0).getClass());
+
+ // Test push/pull
+ mongoDB.pushItemToList(mary, "kids", "Pauline");
+ mongoDB.pullItemFromList(mary, "kids", "Paul");
+
+ Address addr3 = new Address();
+ addr3.setNumber(6);
+ addr3.setStreet("Broadway");
+ mongoDB.pushItemToList(mary, "addresses", addr3);
+
+ mary = mongoDB.loadObject(Person.class, mary.getId());
+ Assert.assertEquals(3, mary.getKids().size());
+ Assert.assertTrue(mary.getKids().contains("Pauline"));
+ Assert.assertFalse(mary.getKids().contains("Paul"));
+ Assert.assertEquals(3, mary.getAddresses().size());
+ Address mainAddress = mary.getMainAddress();
+ Assert.assertEquals("Elm", mainAddress.getStreet());
+ Assert.assertEquals(5, mainAddress.getNumber());
+ Assert.assertEquals(Person.Gender.FEMALE, mary.getGender());
+ Assert.assertTrue(mary.getGenders().contains(Person.Gender.FEMALE));
+ }
+}
diff --git a/model/mongo/src/test/java/org/keycloak/models/mongo/test/Person.java b/model/mongo/src/test/java/org/keycloak/models/mongo/test/Person.java
new file mode 100644
index 0000000..ab2ded3
--- /dev/null
+++ b/model/mongo/src/test/java/org/keycloak/models/mongo/test/Person.java
@@ -0,0 +1,101 @@
+package org.keycloak.models.mongo.test;
+
+import java.util.List;
+
+import org.keycloak.models.mongo.api.AbstractNoSQLObject;
+import org.keycloak.models.mongo.api.NoSQLCollection;
+import org.keycloak.models.mongo.api.NoSQLField;
+import org.keycloak.models.mongo.api.NoSQLId;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NoSQLCollection(collectionName = "persons")
+public class Person extends AbstractNoSQLObject {
+
+ private String id;
+ private String firstName;
+ private int age;
+ private List<String> kids;
+ private List<Address> addresses;
+ private Address mainAddress;
+ private Gender gender;
+ private List<Gender> genders;
+
+
+ @NoSQLId
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @NoSQLField
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ @NoSQLField
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ @NoSQLField
+ public Gender getGender() {
+ return gender;
+ }
+
+ public void setGender(Gender gender) {
+ this.gender = gender;
+ }
+
+ @NoSQLField
+ public List<Gender> getGenders() {
+ return genders;
+ }
+
+ public void setGenders(List<Gender> genders) {
+ this.genders = genders;
+ }
+
+ @NoSQLField
+ public List<String> getKids() {
+ return kids;
+ }
+
+ public void setKids(List<String> kids) {
+ this.kids = kids;
+ }
+
+ @NoSQLField
+ public List<Address> getAddresses() {
+ return addresses;
+ }
+
+ public void setAddresses(List<Address> addresses) {
+ this.addresses = addresses;
+ }
+
+ @NoSQLField
+ public Address getMainAddress() {
+ return mainAddress;
+ }
+
+ public void setMainAddress(Address mainAddress) {
+ this.mainAddress = mainAddress;
+ }
+
+ public static enum Gender {
+ MALE, FEMALE
+ }
+}
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/PicketlinkKeycloakSession.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/PicketlinkKeycloakSession.java
index 4139e03..8e2a75d 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/PicketlinkKeycloakSession.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/PicketlinkKeycloakSession.java
@@ -6,6 +6,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.picketlink.mappings.RealmData;
import org.keycloak.models.picketlink.relationships.RealmAdminRelationship;
+import org.keycloak.models.utils.KeycloakSessionUtils;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.RelationshipManager;
import org.picketlink.idm.query.RelationshipQuery;
@@ -25,11 +26,6 @@ public class PicketlinkKeycloakSession implements KeycloakSession {
protected PartitionManager partitionManager;
protected EntityManager entityManager;
- private static AtomicLong counter = new AtomicLong(1);
- public static String generateId() {
- return counter.getAndIncrement() + "-" + System.currentTimeMillis();
- }
-
public PicketlinkKeycloakSession(PartitionManager partitionManager, EntityManager entityManager) {
this.partitionManager = partitionManager;
this.entityManager = entityManager;
@@ -50,7 +46,7 @@ public class PicketlinkKeycloakSession implements KeycloakSession {
@Override
public RealmAdapter createRealm(String name) {
- return createRealm(generateId(), name);
+ return createRealm(KeycloakSessionUtils.generateId(), name);
}
@Override
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
index 6c62007..d5412ce 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/RealmAdapter.java
@@ -819,6 +819,7 @@ public class RealmAdapter implements RealmModel {
RelationshipQuery<SocialLinkRelationship> query = getRelationshipManager().createRelationshipQuery(SocialLinkRelationship.class);
query.setParameter(SocialLinkRelationship.SOCIAL_PROVIDER, socialLink.getSocialProvider());
query.setParameter(SocialLinkRelationship.SOCIAL_USERNAME, socialLink.getSocialUsername());
+ query.setParameter(SocialLinkRelationship.REALM, realm.getName());
List<SocialLinkRelationship> results = query.getResultList();
if (results.isEmpty()) {
return null;
@@ -850,6 +851,7 @@ public class RealmAdapter implements RealmModel {
relationship.setUser(((UserAdapter)user).getUser());
relationship.setSocialProvider(socialLink.getSocialProvider());
relationship.setSocialUsername(socialLink.getSocialUsername());
+ relationship.setRealm(realm.getName());
getRelationshipManager().add(relationship);
}
@@ -860,6 +862,7 @@ public class RealmAdapter implements RealmModel {
relationship.setUser(((UserAdapter)user).getUser());
relationship.setSocialProvider(socialLink.getSocialProvider());
relationship.setSocialUsername(socialLink.getSocialUsername());
+ relationship.setRealm(realm.getName());
getRelationshipManager().remove(relationship);
}
diff --git a/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java b/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java
index e9be9d4..da8f04f 100755
--- a/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java
+++ b/model/picketlink/src/main/java/org/keycloak/models/picketlink/relationships/SocialLinkRelationship.java
@@ -3,6 +3,7 @@ package org.keycloak.models.picketlink.relationships;
import org.picketlink.idm.model.AbstractAttributedType;
import org.picketlink.idm.model.Attribute;
import org.picketlink.idm.model.Relationship;
+import org.picketlink.idm.model.annotation.AttributeProperty;
import org.picketlink.idm.model.sample.User;
import org.picketlink.idm.query.AttributeParameter;
import org.picketlink.idm.query.RelationshipQueryParameter;
@@ -21,6 +22,10 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re
public static final AttributeParameter SOCIAL_PROVIDER = new AttributeParameter("socialProvider");
public static final AttributeParameter SOCIAL_USERNAME = new AttributeParameter("socialUsername");
+ // realm is needed to allow searching as combination socialUsername+socialProvider may not be unique
+ // (Same user could have mapped same facebook account to username "foo" in "realm1" and to username "bar" in "realm2")
+ public static final AttributeParameter REALM = new AttributeParameter("realm");
+
public static final RelationshipQueryParameter USER = new RelationshipQueryParameter() {
@Override
@@ -39,6 +44,7 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re
this.user = user;
}
+ @AttributeProperty
public String getSocialProvider() {
return (String)getAttribute("socialProvider").getValue();
}
@@ -47,6 +53,7 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re
setAttribute(new Attribute<String>("socialProvider", socialProvider));
}
+ @AttributeProperty
public String getSocialUsername() {
return (String)getAttribute("socialUsername").getValue();
}
@@ -54,4 +61,13 @@ public class SocialLinkRelationship extends AbstractAttributedType implements Re
public void setSocialUsername(String socialProviderUserId) {
setAttribute(new Attribute<String>("socialUsername", socialProviderUserId));
}
+
+ @AttributeProperty
+ public String getRealm() {
+ return (String)getAttribute("realm").getValue();
+ }
+
+ public void setRealm(String realm) {
+ setAttribute(new Attribute<String>("realm", realm));
+ }
}
model/pom.xml 1(+1 -0)
diff --git a/model/pom.xml b/model/pom.xml
index c6a4a9e..7e2fca5 100755
--- a/model/pom.xml
+++ b/model/pom.xml
@@ -37,5 +37,6 @@
<module>api</module>
<module>picketlink</module>
<module>jpa</module>
+ <!--<module>mongo</module>-->
</modules>
</project>
pom.xml 85(+80 -5)
diff --git a/pom.xml b/pom.xml
index d08e744..61503c5 100755
--- a/pom.xml
+++ b/pom.xml
@@ -12,6 +12,14 @@
<resteasy.version>3.0.4.Final</resteasy.version>
<undertow.version>1.0.0.Beta12</undertow.version>
<picketlink.version>2.5.0.Beta6</picketlink.version>
+ <mongo.driver.version>2.11.2</mongo.driver.version>
+ <jboss.logging.version>3.1.1.GA</jboss.logging.version>
+ <hibernate.javax.persistence.version>1.0.1.Final</hibernate.javax.persistence.version>
+ <hibernate.entitymanager.version>3.6.6.Final</hibernate.entitymanager.version>
+ <h2.version>1.3.161</h2.version>
+ <dom4j.version>1.6.1</dom4j.version>
+ <mysql.version>5.1.25</mysql.version>
+ <slf4j.version>1.6.1</slf4j.version>
</properties>
<url>http://keycloak.org</url>
@@ -150,6 +158,11 @@
<version>1.0.1.Final</version>
</dependency>
<dependency>
+ <groupId>org.jboss.spec.javax.servlet</groupId>
+ <artifactId>jboss-servlet-api_3.1_spec</artifactId>
+ <version>1.0.0.Beta1</version>
+ </dependency>
+ <dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-common</artifactId>
<version>${picketlink.version}</version>
@@ -177,7 +190,7 @@
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
- <version>3.1.1.GA</version>
+ <version>${jboss.logging.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
@@ -187,7 +200,17 @@
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.0-api</artifactId>
- <version>1.0.1.Final</version>
+ <version>${hibernate.javax.persistence.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <version>${h2.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.hibernate</groupId>
+ <artifactId>hibernate-entitymanager</artifactId>
+ <version>${hibernate.entitymanager.version}</version>
</dependency>
<dependency>
<groupId>com.google.api-client</groupId>
@@ -236,9 +259,9 @@
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>1.3.1b</version>
- </dependency>
+ </dependency>
- <!-- Selenium -->
+ <!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
@@ -248,7 +271,39 @@
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-chrome-driver</artifactId>
<version>2.35.0</version>
- </dependency>
+ </dependency>
+ <dependency>
+ <groupId>org.mongodb</groupId>
+ <artifactId>mongo-java-driver</artifactId>
+ <version>2.11.2</version>
+ </dependency>
+ <dependency>
+ <groupId>de.flapdoodle.embed</groupId>
+ <artifactId>de.flapdoodle.embed.mongo</artifactId>
+ <version>1.27</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.jmeter</groupId>
+ <artifactId>ApacheJMeter_java</artifactId>
+ <version>2.9</version>
+ </dependency>
+ <dependency>
+ <groupId>dom4j</groupId>
+ <artifactId>dom4j</artifactId>
+ <version>${dom4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+ <!-- Needed for picketlink perf test -->
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <version>${mysql.version}</version>
+ </dependency>
+
</dependencies>
</dependencyManagement>
@@ -330,6 +385,26 @@
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
+ <plugin>
+ <groupId>com.lazerycode.jmeter</groupId>
+ <artifactId>jmeter-maven-plugin</artifactId>
+ <version>1.8.1</version>
+ </plugin>
+ <plugin>
+ <groupId>com.lazerycode.jmeter</groupId>
+ <artifactId>jmeter-analysis-maven-plugin</artifactId>
+ <version>1.0.4</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.2</version>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <version>1.2.1</version>
+ </plugin>
</plugins>
</pluginManagement>
services/pom.xml 23(+21 -2)
diff --git a/services/pom.xml b/services/pom.xml
index 4d8075f..187d7be 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -34,6 +34,12 @@
<artifactId>keycloak-model-picketlink</artifactId>
<version>${project.version}</version>
</dependency>
+
+ <!--<dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-mongo</artifactId>
+ <version>${project.version}</version>
+ </dependency>-->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId>
@@ -145,6 +151,21 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-common</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mongodb</groupId>
+ <artifactId>mongo-java-driver</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>de.flapdoodle.embed</groupId>
+ <artifactId>de.flapdoodle.embed.mongo</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
@@ -157,13 +178,11 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
- <version>1.3.161</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
- <version>3.6.6.Final</version>
<scope>test</scope>
</dependency>
<dependency>
diff --git a/services/src/main/java/org/keycloak/services/listeners/MongoRunnerListener.java b/services/src/main/java/org/keycloak/services/listeners/MongoRunnerListener.java
new file mode 100644
index 0000000..f0df0a6
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/listeners/MongoRunnerListener.java
@@ -0,0 +1,53 @@
+package org.keycloak.services.listeners;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+import de.flapdoodle.embed.mongo.MongodExecutable;
+import de.flapdoodle.embed.mongo.MongodProcess;
+import de.flapdoodle.embed.mongo.MongodStarter;
+import de.flapdoodle.embed.mongo.config.MongodConfig;
+import de.flapdoodle.embed.mongo.distribution.Version;
+import de.flapdoodle.embed.process.runtime.Network;
+import org.jboss.resteasy.logging.Logger;
+import org.keycloak.services.utils.PropertiesManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MongoRunnerListener implements ServletContextListener {
+
+ protected static final Logger logger = Logger.getLogger(MongoRunnerListener.class);
+
+ private MongodExecutable mongodExe;
+ private MongodProcess mongod;
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ if (PropertiesManager.bootstrapEmbeddedMongoAtContextInit()) {
+ int port = PropertiesManager.getMongoPort();
+ logger.info("Going to start embedded MongoDB on port=" + port);
+
+ try {
+ mongodExe = MongodStarter.getDefaultInstance().prepare(new MongodConfig(Version.V2_0_5, port, Network.localhostIsIPv6()));
+ mongod = mongodExe.start();
+ } catch (Exception e) {
+ logger.warn("Couldn't start Embedded Mongo on port " + port + ". Maybe it's already started? Cause: " + e.getClass() + " " + e.getMessage());
+ if (logger.isDebugEnabled()) {
+ logger.debug("Failed to start MongoDB", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ if (mongodExe != null) {
+ if (mongod != null) {
+ logger.info("Going to stop embedded MongoDB.");
+ mongod.stop();
+ }
+ mongodExe.stop();
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
index cdb588e..133b062 100755
--- a/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
+++ b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java
@@ -125,7 +125,7 @@ public class FormFlows {
// TODO find a better way to obtain contextPath
// Getting context path by removing "rest/" substring from the BaseUri path
- formDataBean.setContextPath(requestURI.substring(0,requestURI.length()-5));
+ formDataBean.setContextPath(requestURI.substring(0, requestURI.length() - 6));
formDataBean.setSocialRegistration(socialRegistration);
// Find the service and process relevant template
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 49855b9..0340751 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -7,6 +7,7 @@ import org.keycloak.models.picketlink.PicketlinkKeycloakSession;
import org.keycloak.models.picketlink.PicketlinkKeycloakSessionFactory;
import org.keycloak.models.picketlink.mappings.ApplicationEntity;
import org.keycloak.models.picketlink.mappings.RealmEntity;
+import org.keycloak.services.utils.PropertiesManager;
import org.keycloak.social.SocialRequestManager;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.config.IdentityConfigurationBuilder;
@@ -21,6 +22,8 @@ import javax.persistence.Persistence;
import javax.servlet.ServletContext;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
+
+import java.lang.reflect.Constructor;
import java.util.HashSet;
import java.util.Set;
@@ -29,6 +32,7 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class KeycloakApplication extends Application {
+
protected Set<Object> singletons = new HashSet<Object>();
protected Set<Class<?>> classes = new HashSet<Class<?>>();
@@ -54,10 +58,36 @@ public class KeycloakApplication extends Application {
}
public static KeycloakSessionFactory buildSessionFactory() {
+ if (PropertiesManager.isMongoSessionFactory()) {
+ return buildMongoDBSessionFactory();
+ } else if (PropertiesManager.isPicketlinkSessionFactory()) {
+ return buildPicketlinkSessionFactory();
+ } else {
+ throw new IllegalStateException("Unknown session factory type: " + PropertiesManager.getSessionFactoryType());
+ }
+ }
+
+ private static KeycloakSessionFactory buildPicketlinkSessionFactory() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("keycloak-identity-store");
return new PicketlinkKeycloakSessionFactory(emf, buildPartitionManager());
}
+ private static KeycloakSessionFactory buildMongoDBSessionFactory() {
+ String host = PropertiesManager.getMongoHost();
+ int port = PropertiesManager.getMongoPort();
+ String dbName = PropertiesManager.getMongoDbName();
+ boolean dropDatabaseOnStartup = PropertiesManager.dropDatabaseOnStartup();
+
+ // Create MongoDBSessionFactory via reflection now
+ try {
+ Class<? extends KeycloakSessionFactory> mongoDBSessionFactoryClass = (Class<? extends KeycloakSessionFactory>)Class.forName("org.keycloak.models.mongo.keycloak.adapters.MongoDBSessionFactory");
+ Constructor<? extends KeycloakSessionFactory> constr = mongoDBSessionFactoryClass.getConstructor(String.class, int.class, String.class, boolean.class);
+ return constr.newInstance(host, port, dbName, dropDatabaseOnStartup);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public KeycloakSessionFactory getFactory() {
return factory;
}
diff --git a/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java b/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java
new file mode 100644
index 0000000..ee16547
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java
@@ -0,0 +1,82 @@
+package org.keycloak.services.utils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PropertiesManager {
+
+ private static final String SESSION_FACTORY = "keycloak.sessionFactory";
+ public static final String SESSION_FACTORY_PICKETLINK = "picketlink";
+ public static final String SESSION_FACTORY_MONGO = "mongo";
+
+ private static final String MONGO_HOST = "keycloak.mongodb.host";
+ private static final String MONGO_PORT = "keycloak.mongodb.port";
+ private static final String MONGO_DB_NAME = "keycloak.mongodb.databaseName";
+ private static final String MONGO_DROP_DB_ON_STARTUP = "keycloak.mongodb.dropDatabaseOnStartup";
+ private static final String BOOTSTRAP_EMBEDDED_MONGO_AT_CONTEXT_INIT = "keycloak.mongodb.bootstrapEmbeddedMongoAtContextInit";
+
+ // Port where embedded MongoDB will be started during keycloak bootstrap. Same port will be used by KeycloakApplication then
+ private static final int MONGO_DEFAULT_PORT_KEYCLOAK_WAR_EMBEDDED = 37017;
+
+ // Port where MongoDB instance is normally started on linux. This port should be used if we're not starting embedded instance (keycloak.mongodb.bootstrapEmbeddedMongoAtContextInit is false)
+ private static final int MONGO_DEFAULT_PORT_KEYCLOAK_WAR = 27017;
+
+ // Port where unit tests will start embedded MongoDB instance
+ public static final int MONGO_DEFAULT_PORT_UNIT_TESTS = 27777;
+
+ public static String getSessionFactoryType() {
+ return System.getProperty(SESSION_FACTORY, SESSION_FACTORY_PICKETLINK);
+ }
+
+ public static void setSessionFactoryType(String sessionFactoryType) {
+ System.setProperty(SESSION_FACTORY, sessionFactoryType);
+ }
+
+ public static void setDefaultSessionFactoryType() {
+ System.setProperty(SESSION_FACTORY, SESSION_FACTORY_PICKETLINK);
+ }
+
+ public static boolean isMongoSessionFactory() {
+ return getSessionFactoryType().equals(SESSION_FACTORY_MONGO);
+ }
+
+ public static boolean isPicketlinkSessionFactory() {
+ return getSessionFactoryType().equals(SESSION_FACTORY_PICKETLINK);
+ }
+
+ public static String getMongoHost() {
+ return System.getProperty(MONGO_HOST, "localhost");
+ }
+
+ public static void setMongoHost(String mongoHost) {
+ System.setProperty(MONGO_HOST, mongoHost);
+ }
+
+ public static int getMongoPort() {
+ return Integer.parseInt(System.getProperty(MONGO_PORT, String.valueOf(MONGO_DEFAULT_PORT_KEYCLOAK_WAR_EMBEDDED)));
+ }
+
+ public static void setMongoPort(int mongoPort) {
+ System.setProperty(MONGO_PORT, String.valueOf(mongoPort));
+ }
+
+ public static String getMongoDbName() {
+ return System.getProperty(MONGO_DB_NAME, "keycloak");
+ }
+
+ public static void setMongoDbName(String mongoMongoDbName) {
+ System.setProperty(MONGO_DB_NAME, mongoMongoDbName);
+ }
+
+ public static boolean dropDatabaseOnStartup() {
+ return Boolean.parseBoolean(System.getProperty(MONGO_DROP_DB_ON_STARTUP, "true"));
+ }
+
+ public static void setDropDatabaseOnStartup(boolean dropDatabaseOnStartup) {
+ System.setProperty(MONGO_DROP_DB_ON_STARTUP, String.valueOf(dropDatabaseOnStartup));
+ }
+
+ public static boolean bootstrapEmbeddedMongoAtContextInit() {
+ return isMongoSessionFactory() && Boolean.parseBoolean(System.getProperty(BOOTSTRAP_EMBEDDED_MONGO_AT_CONTEXT_INIT, "true"));
+ }
+}
diff --git a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java b/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java
index ce3ed3d..a2049fd 100755
--- a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java
+++ b/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java
@@ -18,24 +18,20 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.resources.KeycloakApplication;
+import org.keycloak.test.common.AbstractKeycloakTest;
+import org.keycloak.test.common.SessionFactoryTestContext;
import org.picketlink.idm.credential.util.TimeBasedOTP;
-public class AuthenticationManagerTest {
+public class AuthenticationManagerTest extends AbstractKeycloakTest {
- private RealmManager adapter;
private AuthenticationManager am;
- private KeycloakSessionFactory factory;
private MultivaluedMap<String, String> formData;
- private KeycloakSession identitySession;
private TimeBasedOTP otp;
private RealmModel realm;
private UserModel user;
- @After
- public void after() throws Exception {
- identitySession.getTransaction().commit();
- identitySession.close();
- factory.close();
+ public AuthenticationManagerTest(SessionFactoryTestContext testContext) {
+ super(testContext);
}
@Test
@@ -134,12 +130,8 @@ public class AuthenticationManagerTest {
@Before
public void before() throws Exception {
- factory = KeycloakApplication.buildSessionFactory();
- identitySession = factory.createSession();
- identitySession.getTransaction().begin();
- adapter = new RealmManager(identitySession);
-
- realm = adapter.createRealm("Test");
+ super.before();
+ realm = getRealmManager().createRealm("Test");
realm.setAccessCodeLifespan(100);
realm.setCookieLoginAllowed(true);
realm.setEnabled(true);
diff --git a/services/src/test/java/org/keycloak/test/AdapterTest.java b/services/src/test/java/org/keycloak/test/AdapterTest.java
index 1bdaa82..41aa8e1 100755
--- a/services/src/test/java/org/keycloak/test/AdapterTest.java
+++ b/services/src/test/java/org/keycloak/test/AdapterTest.java
@@ -12,6 +12,8 @@ import org.keycloak.services.managers.OAuthClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.resources.KeycloakApplication;
+import org.keycloak.test.common.AbstractKeycloakTest;
+import org.keycloak.test.common.SessionFactoryTestContext;
import java.util.HashSet;
@@ -24,30 +26,16 @@ import java.util.StringTokenizer;
* @version $Revision: 1 $
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class AdapterTest {
- private KeycloakSessionFactory factory;
- private KeycloakSession identitySession;
- private RealmManager adapter;
+public class AdapterTest extends AbstractKeycloakTest {
private RealmModel realmModel;
- @Before
- public void before() throws Exception {
- factory = KeycloakApplication.buildSessionFactory();
- identitySession = factory.createSession();
- identitySession.getTransaction().begin();
- adapter = new RealmManager(identitySession);
- }
-
- @After
- public void after() throws Exception {
- identitySession.getTransaction().commit();
- identitySession.close();
- factory.close();
+ public AdapterTest(SessionFactoryTestContext testContext) {
+ super(testContext);
}
@Test
public void installTest() throws Exception {
- new InstallationManager().install(adapter);
+ new InstallationManager().install(getRealmManager());
}
@@ -63,7 +51,7 @@ public class AdapterTest {
@Test
public void test1CreateRealm() throws Exception {
- realmModel = adapter.createRealm("JUGGLER");
+ realmModel = getRealmManager().createRealm("JUGGLER");
realmModel.setAccessCodeLifespan(100);
realmModel.setAccessCodeLifespanUserAction(600);
realmModel.setCookieLoginAllowed(true);
@@ -76,7 +64,7 @@ public class AdapterTest {
realmModel.addDefaultRole("foo");
System.out.println(realmModel.getId());
- realmModel = adapter.getRealm(realmModel.getId());
+ realmModel = getRealmManager().getRealm(realmModel.getId());
Assert.assertNotNull(realmModel);
Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100);
Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction());
@@ -153,6 +141,8 @@ public class AdapterTest {
user.setEmail("bburke@redhat.com");
}
+ RealmManager adapter = getRealmManager();
+
{
List<UserModel> userModels = adapter.searchUsers("total junk query", realmModel);
Assert.assertEquals(userModels.size(), 0);
diff --git a/services/src/test/java/org/keycloak/test/common/AbstractKeycloakTest.java b/services/src/test/java/org/keycloak/test/common/AbstractKeycloakTest.java
new file mode 100644
index 0000000..baaa9f6
--- /dev/null
+++ b/services/src/test/java/org/keycloak/test/common/AbstractKeycloakTest.java
@@ -0,0 +1,96 @@
+package org.keycloak.test.common;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.resources.KeycloakApplication;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@RunWith(Parameterized.class)
+public abstract class AbstractKeycloakTest {
+
+ protected static final SessionFactoryTestContext[] TEST_CONTEXTS;
+
+ private final SessionFactoryTestContext testContext;
+ private KeycloakSessionFactory factory;
+ private KeycloakSession identitySession;
+ private RealmManager realmManager;
+
+ // STATIC METHODS
+
+ static
+ {
+ // TODO: MongoDB disabled by default
+ TEST_CONTEXTS = new SessionFactoryTestContext[] {
+ new PicketlinkSessionFactoryTestContext(),
+ // new MongoDBSessionFactoryTestContext()
+ };
+ }
+
+ @Parameterized.Parameters
+ public static Iterable<Object[]> parameters() {
+ List<Object[]> params = new ArrayList<Object[]>();
+
+ for (SessionFactoryTestContext testContext : TEST_CONTEXTS) {
+ params.add(new Object[] {testContext});
+ }
+ return params;
+ }
+
+ @BeforeClass
+ public static void baseBeforeClass() {
+ for (SessionFactoryTestContext testContext : TEST_CONTEXTS) {
+ testContext.beforeTestClass();
+ }
+ }
+
+ @AfterClass
+ public static void baseAfterClass() {
+ for (SessionFactoryTestContext testContext : TEST_CONTEXTS) {
+ testContext.afterTestClass();
+ }
+ }
+
+ // NON-STATIC METHODS
+
+ public AbstractKeycloakTest(SessionFactoryTestContext testContext) {
+ this.testContext = testContext;
+ }
+
+ @Before
+ public void before() throws Exception {
+ testContext.initEnvironment();
+ factory = KeycloakApplication.buildSessionFactory();
+ identitySession = factory.createSession();
+ identitySession.getTransaction().begin();
+ realmManager = new RealmManager(identitySession);
+ }
+
+ @After
+ public void after() throws Exception {
+ identitySession.getTransaction().commit();
+ identitySession.close();
+ factory.close();
+ }
+
+ protected RealmManager getRealmManager() {
+ return realmManager;
+ }
+
+ protected KeycloakSession getIdentitySession() {
+ return identitySession;
+ }
+
+}
diff --git a/services/src/test/java/org/keycloak/test/common/MongoDBSessionFactoryTestContext.java b/services/src/test/java/org/keycloak/test/common/MongoDBSessionFactoryTestContext.java
new file mode 100644
index 0000000..def3f34
--- /dev/null
+++ b/services/src/test/java/org/keycloak/test/common/MongoDBSessionFactoryTestContext.java
@@ -0,0 +1,58 @@
+package org.keycloak.test.common;
+
+import de.flapdoodle.embed.mongo.MongodExecutable;
+import de.flapdoodle.embed.mongo.MongodProcess;
+import de.flapdoodle.embed.mongo.MongodStarter;
+import de.flapdoodle.embed.mongo.config.MongodConfig;
+import de.flapdoodle.embed.mongo.distribution.Version;
+import de.flapdoodle.embed.process.runtime.Network;
+import org.jboss.resteasy.logging.Logger;
+import org.keycloak.services.resources.KeycloakApplication;
+import org.keycloak.services.utils.PropertiesManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MongoDBSessionFactoryTestContext implements SessionFactoryTestContext {
+
+ protected static final Logger logger = Logger.getLogger(MongoDBSessionFactoryTestContext.class);
+ private static final int PORT = PropertiesManager.MONGO_DEFAULT_PORT_UNIT_TESTS;
+
+ private MongodExecutable mongodExe;
+ private MongodProcess mongod;
+
+ @Override
+ public void beforeTestClass() {
+ logger.info("Bootstrapping MongoDB on localhost, port " + PORT);
+ try {
+ mongodExe = MongodStarter.getDefaultInstance().prepare(new MongodConfig(Version.V2_0_5, PORT, Network.localhostIsIPv6()));
+ mongod = mongodExe.start();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ logger.info("MongoDB bootstrapped successfully");
+ }
+
+ @Override
+ public void afterTestClass() {
+ if (mongodExe != null) {
+ if (mongod != null) {
+ mongod.stop();
+ }
+ mongodExe.stop();
+ }
+ logger.info("MongoDB stopped successfully");
+
+ // Reset this, so other tests are not affected
+ PropertiesManager.setDefaultSessionFactoryType();
+ }
+
+ @Override
+ public void initEnvironment() {
+ PropertiesManager.setSessionFactoryType(PropertiesManager.SESSION_FACTORY_MONGO);
+ PropertiesManager.setMongoHost("localhost");
+ PropertiesManager.setMongoPort(PORT);
+ PropertiesManager.setMongoDbName("keycloakTest");
+ PropertiesManager.setDropDatabaseOnStartup(true);
+ }
+}
diff --git a/services/src/test/java/org/keycloak/test/common/PicketlinkSessionFactoryTestContext.java b/services/src/test/java/org/keycloak/test/common/PicketlinkSessionFactoryTestContext.java
new file mode 100644
index 0000000..80b2cbc
--- /dev/null
+++ b/services/src/test/java/org/keycloak/test/common/PicketlinkSessionFactoryTestContext.java
@@ -0,0 +1,25 @@
+package org.keycloak.test.common;
+
+import org.keycloak.services.resources.KeycloakApplication;
+import org.keycloak.services.utils.PropertiesManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PicketlinkSessionFactoryTestContext implements SessionFactoryTestContext {
+
+ @Override
+ public void beforeTestClass() {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public void afterTestClass() {
+ //To change body of implemented methods use File | Settings | File Templates.
+ }
+
+ @Override
+ public void initEnvironment() {
+ PropertiesManager.setSessionFactoryType(PropertiesManager.SESSION_FACTORY_PICKETLINK);
+ }
+}
diff --git a/services/src/test/java/org/keycloak/test/common/SessionFactoryTestContext.java b/services/src/test/java/org/keycloak/test/common/SessionFactoryTestContext.java
new file mode 100644
index 0000000..a35cfd2
--- /dev/null
+++ b/services/src/test/java/org/keycloak/test/common/SessionFactoryTestContext.java
@@ -0,0 +1,17 @@
+package org.keycloak.test.common;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface SessionFactoryTestContext {
+
+ void beforeTestClass();
+
+ void afterTestClass();
+
+ /**
+ * Init system properties (or other configuration) to ensure that KeycloakApplication.buildSessionFactory() will return correct
+ * instance of KeycloakSessionFactory for our test
+ */
+ void initEnvironment();
+}
diff --git a/services/src/test/java/org/keycloak/test/ImportTest.java b/services/src/test/java/org/keycloak/test/ImportTest.java
index d426d4e..33348ed 100755
--- a/services/src/test/java/org/keycloak/test/ImportTest.java
+++ b/services/src/test/java/org/keycloak/test/ImportTest.java
@@ -19,6 +19,8 @@ import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.services.resources.SaasService;
+import org.keycloak.test.common.AbstractKeycloakTest;
+import org.keycloak.test.common.SessionFactoryTestContext;
import java.util.List;
import java.util.Set;
@@ -28,29 +30,15 @@ import java.util.Set;
* @version $Revision: 1 $
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class ImportTest {
- private KeycloakSessionFactory factory;
- private KeycloakSession identitySession;
- private RealmManager manager;
- private RealmModel realmModel;
-
- @Before
- public void before() throws Exception {
- factory = KeycloakApplication.buildSessionFactory();
- identitySession = factory.createSession();
- identitySession.getTransaction().begin();
- manager = new RealmManager(identitySession);
- }
+public class ImportTest extends AbstractKeycloakTest {
- @After
- public void after() throws Exception {
- identitySession.getTransaction().commit();
- identitySession.close();
- factory.close();
+ public ImportTest(SessionFactoryTestContext testContext) {
+ super(testContext);
}
@Test
public void install() throws Exception {
+ RealmManager manager = getRealmManager();
RealmModel defaultRealm = manager.createRealm(RealmModel.DEFAULT_REALM, RealmModel.DEFAULT_REALM);
defaultRealm.setName(RealmModel.DEFAULT_REALM);
defaultRealm.setEnabled(true);
@@ -93,7 +81,7 @@ public class ImportTest {
List<ApplicationModel> resources = realm.getApplications();
Assert.assertEquals(2, resources.size());
- List<RealmModel> realms = identitySession.getRealms(admin);
+ List<RealmModel> realms = getIdentitySession().getRealms(admin);
Assert.assertEquals(1, realms.size());
// Test scope relationship
@@ -129,6 +117,7 @@ public class ImportTest {
@Test
public void install2() throws Exception {
+ RealmManager manager = getRealmManager();
RealmModel defaultRealm = manager.createRealm(RealmModel.DEFAULT_REALM, RealmModel.DEFAULT_REALM);
defaultRealm.setName(RealmModel.DEFAULT_REALM);
defaultRealm.setEnabled(true);
testsuite/integration/pom.xml 253(+253 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
new file mode 100644
index 0000000..f0f3eea
--- /dev/null
+++ b/testsuite/integration/pom.xml
@@ -0,0 +1,253 @@
+<?xml version="1.0"?>
+<project>
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-alpha-1</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-testsuite-integration</artifactId>
+ <name>Keycloak Integration TestSuite</name>
+ <description />
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-as7-adapter</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.spec.javax.servlet</groupId>
+ <artifactId>jboss-servlet-api_3.1_spec</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk16</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-admin-ui</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-admin-ui-styles</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-services</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-social-core</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.keycloak</groupId>
+ <artifactId>keycloak-social-facebook</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-forms</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-idm-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-common</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>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-config</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-jaxrs</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>jaxrs-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-crypto</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>jose-jwt</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-undertow</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.undertow</groupId>
+ <artifactId>undertow-servlet</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.undertow</groupId>
+ <artifactId>undertow-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-core-asl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-xc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.hibernate.javax.persistence</groupId>
+ <artifactId>hibernate-jpa-2.0-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <version>1.3.161</version>
+ </dependency>
+ <dependency>
+ <groupId>org.hibernate</groupId>
+ <artifactId>hibernate-entitymanager</artifactId>
+ <version>3.6.6.Final</version>
+ </dependency>
+ <dependency>
+ <groupId>com.icegreen</groupId>
+ <artifactId>greenmail</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-java</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mongodb</groupId>
+ <artifactId>mongo-java-driver</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>keycloak-server</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>org.keycloak.testutils.KeycloakServer</mainClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>mail-server</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>org.keycloak.testutils.MailServer</mainClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>totp</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>org.keycloak.testutils.TotpGenerator</mainClass>
+ <arguments>
+ <argument>${secret}</argument>
+ </arguments>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
\ No newline at end of file
testsuite/performance/pom.xml 240(+240 -0)
diff --git a/testsuite/performance/pom.xml b/testsuite/performance/pom.xml
new file mode 100644
index 0000000..1cb8220
--- /dev/null
+++ b/testsuite/performance/pom.xml
@@ -0,0 +1,240 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-alpha-1</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-testsuite-performance</artifactId>
+ <name>Keycloak Performance TestSuite</name>
+ <description />
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-services</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-jaxrs</artifactId>
+ <scope>provided</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>jaxrs-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-client</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.jmeter</groupId>
+ <artifactId>ApacheJMeter_java</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>performance-tests</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.lazerycode.jmeter</groupId>
+ <artifactId>jmeter-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>jmeter-tests</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>jmeter</goal>
+ </goals>
+ </execution>
+ </executions>
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-testsuite</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-services</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>jaxrs-api</artifactId>
+ <version>${resteasy.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-jaxrs</artifactId>
+ <version>${resteasy.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <version>${jboss.logging.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-idm-impl</artifactId>
+ <version>${picketlink.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-idm-simple-schema</artifactId>
+ <version>${picketlink.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.picketlink</groupId>
+ <artifactId>picketlink-config</artifactId>
+ <version>${picketlink.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.mongodb</groupId>
+ <artifactId>mongo-java-driver</artifactId>
+ <version>${mongo.driver.version}</version>
+ </dependency>
+
+ <!-- Needed for picketlink -->
+ <dependency>
+ <groupId>org.hibernate.javax.persistence</groupId>
+ <artifactId>hibernate-jpa-2.0-api</artifactId>
+ <version>${hibernate.javax.persistence.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <version>${h2.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.hibernate</groupId>
+ <artifactId>hibernate-entitymanager</artifactId>
+ <version>${hibernate.entitymanager.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>dom4j</groupId>
+ <artifactId>dom4j</artifactId>
+ <version>${dom4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+
+ <!-- Needed just for picketlink perf test -->
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <version>${mysql.version}</version>
+ </dependency>
+
+ </dependencies>
+ </plugin>
+
+ <plugin>
+ <groupId>com.lazerycode.jmeter</groupId>
+ <artifactId>jmeter-analysis-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>jmeter-tests-analyze</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>analyze</goal>
+ </goals>
+ <configuration>
+ <source>${project.build.directory}/jmeter/results/*.jtl</source>
+ <targetDirectory>${project.build.directory}/jmeter/results</targetDirectory>
+ <preserveDirectories>false</preserveDirectories>
+ <writers>
+ <com.lazerycode.jmeter.analyzer.writer.SummaryTextToStdOutWriter/>
+ <!--<com.lazerycode.jmeter.analyzer.writer.SummaryTextToFileWriter/>-->
+ <com.lazerycode.jmeter.analyzer.writer.HtmlWriter/>
+ <!--<com.lazerycode.jmeter.analyzer.writer.DetailsToCsvWriter/>-->
+ <com.lazerycode.jmeter.analyzer.writer.DetailsToHtmlWriter/>
+ <com.lazerycode.jmeter.analyzer.writer.ChartWriter/>
+ </writers>
+
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ </plugins>
+ </build>
+ </profile>
+
+ </profiles>
+</project>
\ No newline at end of file
diff --git a/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/BaseJMeterPerformanceTest.java b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/BaseJMeterPerformanceTest.java
new file mode 100644
index 0000000..efe57c1
--- /dev/null
+++ b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/BaseJMeterPerformanceTest.java
@@ -0,0 +1,140 @@
+package org.keycloak.testsuite.performance;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient;
+import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext;
+import org.apache.jmeter.samplers.SampleResult;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.services.resources.KeycloakApplication;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class BaseJMeterPerformanceTest extends AbstractJavaSamplerClient {
+
+
+ private static FutureTask<KeycloakSessionFactory> factoryProvider = new FutureTask<KeycloakSessionFactory>(new Callable() {
+
+ @Override
+ public KeycloakSessionFactory call() throws Exception {
+ return KeycloakApplication.buildSessionFactory();
+ }
+
+ });
+ private static AtomicInteger counter = new AtomicInteger();
+
+ private KeycloakSessionFactory factory;
+ // private KeycloakSession identitySession;
+ private Worker worker;
+ private boolean setupSuccess = false;
+
+
+ // Executed once per JMeter thread
+ @Override
+ public void setupTest(JavaSamplerContext context) {
+ super.setupTest(context);
+
+ worker = getWorker();
+
+ factory = getFactory();
+ KeycloakSession identitySession = factory.createSession();
+ KeycloakTransaction transaction = identitySession.getTransaction();
+ transaction.begin();
+
+ int workerId = counter.getAndIncrement();
+ try {
+ worker.setup(workerId, identitySession);
+ setupSuccess = true;
+ } finally {
+ if (setupSuccess) {
+ transaction.commit();
+ } else {
+ transaction.rollback();
+ }
+ identitySession.close();
+ }
+ }
+
+ private static KeycloakSessionFactory getFactory() {
+ factoryProvider.run();
+ try {
+ return factoryProvider.get();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ private Worker getWorker() {
+ String workerClass = System.getProperty("keycloak.perf.workerClass");
+ if (workerClass == null) {
+ throw new IllegalArgumentException("System property keycloak.perf.workerClass needs to be provided");
+ }
+
+ try {
+ Class workerClazz = Class.forName(workerClass);
+ return (Worker)workerClazz.newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ @Override
+ public SampleResult runTest(JavaSamplerContext context) {
+ SampleResult result = new SampleResult();
+ result.sampleStart();
+
+ if (!setupSuccess) {
+ getLogger().error("setupTest didn't executed successfully. Skipping");
+ result.setResponseCode("500");
+ result.sampleEnd();
+ result.setSuccessful(true);
+ return result;
+ }
+
+ KeycloakSession identitySession = factory.createSession();
+ KeycloakTransaction transaction = identitySession.getTransaction();
+ try {
+ transaction.begin();
+
+ worker.run(result, identitySession);
+
+ result.setResponseCodeOK();
+ transaction.commit();
+ } catch (Exception e) {
+ getLogger().error("Error during worker processing", e);
+ result.setResponseCode("500");
+ transaction.rollback();
+ } finally {
+ result.sampleEnd();
+ result.setSuccessful(true);
+ identitySession.close();
+ }
+
+ return result;
+ }
+
+
+ // Executed once per JMeter thread
+ @Override
+ public void teardownTest(JavaSamplerContext context) {
+ super.teardownTest(context);
+
+ if (worker != null) {
+ worker.tearDown();
+ }
+
+ // TODO: Assumption is that tearDownTest is executed for each setupTest. Verify if it's always true...
+ if (counter.decrementAndGet() == 0) {
+ if (factory != null) {
+ factory.close();
+ }
+ }
+ }
+}
diff --git a/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateRealmsWorker.java b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateRealmsWorker.java
new file mode 100644
index 0000000..2749998
--- /dev/null
+++ b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateRealmsWorker.java
@@ -0,0 +1,101 @@
+package org.keycloak.testsuite.performance;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.jmeter.samplers.SampleResult;
+import org.apache.jorphan.logging.LoggingManager;
+import org.apache.log.Logger;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.RealmManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CreateRealmsWorker implements Worker {
+
+ private static final Logger log = LoggingManager.getLoggerForClass();
+
+ private static final int NUMBER_OF_REALMS_IN_EACH_REPORT = 100;
+
+ private static AtomicInteger realmCounter = new AtomicInteger(0);
+
+ private int offset;
+ private int appsPerRealm;
+ private int rolesPerRealm;
+ private int defaultRolesPerRealm;
+ private int rolesPerApp;
+ private boolean createRequiredCredentials;
+
+ @Override
+ public void setup(int workerId, KeycloakSession identitySession) {
+ offset = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.realms.offset", Integer.class);
+ appsPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.appsPerRealm", Integer.class);
+ rolesPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.rolesPerRealm", Integer.class);
+ defaultRolesPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.defaultRolesPerRealm", Integer.class);
+ rolesPerApp = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.rolesPerApp", Integer.class);
+ createRequiredCredentials = PerfTestUtils.readSystemProperty("keycloak.perf.createRealms.createRequiredCredentials", Boolean.class);
+
+ realmCounter.compareAndSet(0, offset);
+
+ StringBuilder logBuilder = new StringBuilder("Read setup: ")
+ .append("offset=" + offset)
+ .append(", appsPerRealm=" + appsPerRealm)
+ .append(", rolesPerRealm=" + rolesPerRealm)
+ .append(", defaultRolesPerRealm=" + defaultRolesPerRealm)
+ .append(", rolesPerApp=" + rolesPerApp)
+ .append(", createRequiredCredentials=" + createRequiredCredentials);
+ log.info(logBuilder.toString());
+ }
+
+ @Override
+ public void run(SampleResult result, KeycloakSession identitySession) {
+ int realmNumber = realmCounter.getAndIncrement();
+ String realmName = PerfTestUtils.getRealmName(realmNumber);
+ RealmManager realmManager = new RealmManager(identitySession);
+ RealmModel realm = realmManager.createRealm(realmName, realmName);
+
+ // Add roles
+ for (int i=1 ; i<=rolesPerRealm ; i++) {
+ realm.addRole(PerfTestUtils.getRoleName(realmNumber, i));
+ }
+
+ // Add default roles
+ for (int i=1 ; i<=defaultRolesPerRealm ; i++) {
+ realm.addDefaultRole(PerfTestUtils.getDefaultRoleName(realmNumber, i));
+ }
+
+ // Add applications
+ for (int i=1 ; i<=appsPerRealm ; i++) {
+ ApplicationModel application = realm.addApplication(PerfTestUtils.getApplicationName(realmNumber, i));
+ for (int j=1 ; j<=rolesPerApp ; j++) {
+ application.addRole(PerfTestUtils.getApplicationRoleName(realmNumber, i, j));
+ }
+ }
+
+ // Add required credentials
+ if (createRequiredCredentials) {
+ realmManager.addRequiredCredential(realm, CredentialRepresentation.PASSWORD);
+ realmManager.addResourceRequiredCredential(realm, CredentialRepresentation.PASSWORD);
+ realmManager.addOAuthClientRequiredCredential(realm, CredentialRepresentation.PASSWORD);
+ realmManager.addRequiredCredential(realm, CredentialRepresentation.TOTP);
+ realmManager.addResourceRequiredCredential(realm, CredentialRepresentation.TOTP);
+ realmManager.addOAuthClientRequiredCredential(realm, CredentialRepresentation.TOTP);
+ realmManager.addRequiredCredential(realm, CredentialRepresentation.CLIENT_CERT);
+ realmManager.addResourceRequiredCredential(realm, CredentialRepresentation.CLIENT_CERT);
+ realmManager.addOAuthClientRequiredCredential(realm, CredentialRepresentation.CLIENT_CERT);
+ }
+
+ log.info("Finished creation of realm " + realmName);
+
+ int labelC = ((realmNumber - 1) / NUMBER_OF_REALMS_IN_EACH_REPORT) * NUMBER_OF_REALMS_IN_EACH_REPORT;
+ result.setSampleLabel("CreateRealms " + (labelC + 1) + "-" + (labelC + NUMBER_OF_REALMS_IN_EACH_REPORT));
+ }
+
+ @Override
+ public void tearDown() {
+ }
+
+}
diff --git a/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateUsersWorker.java b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateUsersWorker.java
new file mode 100644
index 0000000..29bd33c
--- /dev/null
+++ b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/CreateUsersWorker.java
@@ -0,0 +1,120 @@
+package org.keycloak.testsuite.performance;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.jmeter.samplers.SampleResult;
+import org.apache.jorphan.logging.LoggingManager;
+import org.apache.log.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.SocialLinkModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.CredentialRepresentation;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class CreateUsersWorker implements Worker {
+
+ private static final Logger log = LoggingManager.getLoggerForClass();
+
+ private static final int NUMBER_OF_USERS_IN_EACH_REPORT = 5000;
+
+ // Total number of users created during whole test
+ private static AtomicInteger totalUserCounter = new AtomicInteger();
+
+ // Adding users will always start from 1. Each worker thread needs to add users to single realm, which is dedicated just for this worker
+ private int userCounterInRealm = 0;
+ private String realmId;
+
+ private int realmsOffset;
+ private boolean addBasicUserAttributes;
+ private boolean addDefaultRoles;
+ private boolean addPassword;
+ private int socialLinksPerUserCount;
+
+ @Override
+ public void setup(int workerId, KeycloakSession identitySession) {
+ realmsOffset = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.realms.offset", Integer.class);
+ addBasicUserAttributes = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.addBasicUserAttributes", Boolean.class);
+ addDefaultRoles = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.addDefaultRoles", Boolean.class);
+ addPassword = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.addPassword", Boolean.class);
+ socialLinksPerUserCount = PerfTestUtils.readSystemProperty("keycloak.perf.createUsers.socialLinksPerUserCount", Integer.class);
+
+ int realmNumber = realmsOffset + workerId;
+ realmId = PerfTestUtils.getRealmName(realmNumber);
+
+ StringBuilder logBuilder = new StringBuilder("Read setup: ")
+ .append("realmsOffset=" + realmsOffset)
+ .append(", addBasicUserAttributes=" + addBasicUserAttributes)
+ .append(", addDefaultRoles=" + addDefaultRoles)
+ .append(", addPassword=" + addPassword)
+ .append(", socialLinksPerUserCount=" + socialLinksPerUserCount)
+ .append(", realmId=" + realmId);
+ log.info(logBuilder.toString());
+ }
+
+ @Override
+ public void run(SampleResult result, KeycloakSession identitySession) {
+ // We need to obtain realm first
+ RealmModel realm = identitySession.getRealm(realmId);
+ if (realm == null) {
+ throw new IllegalStateException("Realm '" + realmId + "' not found");
+ }
+
+ int userNumber = ++userCounterInRealm;
+ int totalUserNumber = totalUserCounter.incrementAndGet();
+
+ String username = PerfTestUtils.getUsername(userNumber);
+
+ UserModel user = realm.addUser(username);
+
+ // Add basic user attributes (NOTE: Actually backend is automatically upgraded during each setter call)
+ if (addBasicUserAttributes) {
+ user.setFirstName(username + "FN");
+ user.setLastName(username + "LN");
+ user.setEmail(username + "@email.com");
+ }
+
+ // Adding default roles of realm to user
+ if (addDefaultRoles) {
+ for (RoleModel role : realm.getDefaultRoles()) {
+ realm.grantRole(user, role);
+ }
+ }
+
+ // Creating password (will be same as username)
+ if (addPassword) {
+ UserCredentialModel password = new UserCredentialModel();
+ password.setType(CredentialRepresentation.PASSWORD);
+ password.setValue(username);
+ realm.updateCredential(user, password);
+ }
+
+ // Creating some socialLinks
+ for (int i=0 ; i<socialLinksPerUserCount ; i++) {
+ String socialProvider;
+ switch (i) {
+ case 0: socialProvider = "facebook"; break;
+ case 1: socialProvider = "twitter"; break;
+ case 2: socialProvider = "google"; break;
+ default: throw new IllegalArgumentException("Total number of socialLinksPerUserCount is " + socialLinksPerUserCount
+ + " which is too big.");
+ }
+
+ SocialLinkModel socialLink = new SocialLinkModel(socialProvider, username);
+ realm.addSocialLink(user, socialLink);
+ }
+
+ log.info("Finished creation of user " + username + " in realm: " + realm.getId());
+
+ int labelC = ((totalUserNumber - 1) / NUMBER_OF_USERS_IN_EACH_REPORT) * NUMBER_OF_USERS_IN_EACH_REPORT;
+ result.setSampleLabel("CreateUsers " + (labelC + 1) + "-" + (labelC + NUMBER_OF_USERS_IN_EACH_REPORT));
+ }
+
+ @Override
+ public void tearDown() {
+ }
+}
diff --git a/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/PerfTestUtils.java b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/PerfTestUtils.java
new file mode 100644
index 0000000..75b3554
--- /dev/null
+++ b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/PerfTestUtils.java
@@ -0,0 +1,46 @@
+package org.keycloak.testsuite.performance;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PerfTestUtils {
+
+ public static <T> T readSystemProperty(String propertyName, Class<T> expectedClass) {
+ String propAsString = System.getProperty(propertyName);
+ if (propAsString == null || propAsString.length() == 0) {
+ throw new IllegalArgumentException("Property '" + propertyName + "' not specified");
+ }
+
+ if (Integer.class.equals(expectedClass)) {
+ return expectedClass.cast(Integer.parseInt(propAsString));
+ } else if (Boolean.class.equals(expectedClass)) {
+ return expectedClass.cast(Boolean.valueOf(propAsString));
+ } else {
+ throw new IllegalArgumentException("Not supported type " + expectedClass);
+ }
+ }
+
+ public static String getRealmName(int realmNumber) {
+ return "realm" + realmNumber;
+ }
+
+ public static String getApplicationName(int realmNumber, int applicationNumber) {
+ return getRealmName(realmNumber) + "application" + applicationNumber;
+ }
+
+ public static String getRoleName(int realmNumber, int roleNumber) {
+ return getRealmName(realmNumber) + "role" + roleNumber;
+ }
+
+ public static String getDefaultRoleName(int realmNumber, int defaultRoleNumber) {
+ return getRealmName(realmNumber) + "defrole" + defaultRoleNumber;
+ }
+
+ public static String getApplicationRoleName(int realmNumber, int applicationNumber, int roleNumber) {
+ return getApplicationName(realmNumber, applicationNumber) + "role" + roleNumber;
+ }
+
+ public static String getUsername(int userNumber) {
+ return "user" + userNumber;
+ }
+}
diff --git a/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/ReadUsersWorker.java b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/ReadUsersWorker.java
new file mode 100644
index 0000000..416cd60
--- /dev/null
+++ b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/ReadUsersWorker.java
@@ -0,0 +1,127 @@
+package org.keycloak.testsuite.performance;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.jmeter.samplers.SampleResult;
+import org.apache.jorphan.logging.LoggingManager;
+import org.apache.log.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.SocialLinkModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ReadUsersWorker implements Worker {
+
+ private static final Logger log = LoggingManager.getLoggerForClass();
+
+ private static final int NUMBER_OF_ITERATIONS_IN_EACH_REPORT = 5000;
+
+ // Total number of iterations read during whole test
+ private static AtomicInteger totalIterationCounter = new AtomicInteger();
+
+ // Reading users will always start from 1. Each worker thread needs to read users to single realm, which is dedicated just for this worker
+ private int userCounterInRealm = 0;
+
+ private int realmsOffset;
+ private int readUsersPerIteration;
+ private int countOfUsersPerRealm;
+ private boolean readRoles;
+ private boolean readScopes;
+ private boolean readPassword;
+ private boolean readSocialLinks;
+ private boolean searchBySocialLinks;
+
+ private String realmId;
+ private int iterationNumber;
+
+ @Override
+ public void setup(int workerId, KeycloakSession identitySession) {
+ realmsOffset = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.realms.offset", Integer.class);
+ readUsersPerIteration = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readUsersPerIteration", Integer.class);
+ countOfUsersPerRealm = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.countOfUsersPerRealm", Integer.class);
+ readRoles = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readRoles", Boolean.class);
+ readScopes = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readScopes", Boolean.class);
+ readPassword = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readPassword", Boolean.class);
+ readSocialLinks = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.readSocialLinks", Boolean.class);
+ searchBySocialLinks = PerfTestUtils.readSystemProperty("keycloak.perf.readUsers.searchBySocialLinks", Boolean.class);
+
+ int realmNumber = realmsOffset + workerId;
+ realmId = PerfTestUtils.getRealmName(realmNumber);
+
+ StringBuilder logBuilder = new StringBuilder("Read setup: ")
+ .append("realmsOffset=" + realmsOffset)
+ .append(", readUsersPerIteration=" + readUsersPerIteration)
+ .append(", countOfUsersPerRealm=" + countOfUsersPerRealm)
+ .append(", readRoles=" + readRoles)
+ .append(", readScopes=" + readScopes)
+ .append(", readPassword=" + readPassword)
+ .append(", readSocialLinks=" + readSocialLinks)
+ .append(", searchBySocialLinks=" + searchBySocialLinks)
+ .append(", realmId=" + realmId);
+ log.info(logBuilder.toString());
+ }
+
+ @Override
+ public void run(SampleResult result, KeycloakSession identitySession) {
+ // We need to obtain realm first
+ RealmModel realm = identitySession.getRealm(realmId);
+ if (realm == null) {
+ throw new IllegalStateException("Realm '" + realmId + "' not found");
+ }
+
+ int totalIterationNumber = totalIterationCounter.incrementAndGet();
+ String lastUsername = null;
+
+ for (int i=0 ; i<readUsersPerIteration ; i++) {
+ ++userCounterInRealm;
+
+ // Start reading users from 1
+ if (userCounterInRealm > countOfUsersPerRealm) {
+ userCounterInRealm = 1;
+ }
+
+ String username = PerfTestUtils.getUsername(userCounterInRealm);
+ lastUsername = username;
+
+ UserModel user = realm.getUser(username);
+
+ // Read roles of user in realm
+ if (readRoles) {
+ realm.getRoleMappings(user);
+ }
+
+ // Read scopes of user in realm
+ if (readScopes) {
+ realm.getScopeMappings(user);
+ }
+
+ // Validate password (shoould be same as username)
+ if (readPassword) {
+ realm.validatePassword(user, username);
+ }
+
+ // Read socialLinks of user
+ if (readSocialLinks) {
+ realm.getSocialLinks(user);
+ }
+
+ // Try to search by social links
+ if (searchBySocialLinks) {
+ SocialLinkModel socialLink = new SocialLinkModel("facebook", username);
+ realm.getUserBySocialLink(socialLink);
+ }
+ }
+
+ log.info("Finished iteration " + ++iterationNumber + " in ReadUsers test for " + realmId + " worker. Last read user " + lastUsername + " in realm: " + realmId);
+
+ int labelC = ((totalIterationNumber - 1) / NUMBER_OF_ITERATIONS_IN_EACH_REPORT) * NUMBER_OF_ITERATIONS_IN_EACH_REPORT;
+ result.setSampleLabel("ReadUsers " + (labelC + 1) + "-" + (labelC + NUMBER_OF_ITERATIONS_IN_EACH_REPORT));
+ }
+
+ @Override
+ public void tearDown() {
+ }
+}
diff --git a/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/RemoveUsersWorker.java b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/RemoveUsersWorker.java
new file mode 100644
index 0000000..262ad93
--- /dev/null
+++ b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/RemoveUsersWorker.java
@@ -0,0 +1,72 @@
+package org.keycloak.testsuite.performance;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.jmeter.samplers.SampleResult;
+import org.apache.jorphan.logging.LoggingManager;
+import org.apache.log.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.utils.PropertiesManager;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RemoveUsersWorker implements Worker {
+
+ private static final Logger log = LoggingManager.getLoggerForClass();
+
+ private static final int NUMBER_OF_USERS_IN_EACH_REPORT = 5000;
+
+ // Total number of users removed during whole test
+ private static AtomicInteger totalUserCounter = new AtomicInteger();
+
+ // Removing users will always start from 1. Each worker thread needs to add users to single realm, which is dedicated just for this worker
+ private int userCounterInRealm = 0;
+ private RealmModel realm;
+
+ private int realmsOffset;
+
+ @Override
+ public void setup(int workerId, KeycloakSession identitySession) {
+ realmsOffset = PerfTestUtils.readSystemProperty("keycloak.perf.removeUsers.realms.offset", Integer.class);
+
+ int realmNumber = realmsOffset + workerId;
+ String realmId = PerfTestUtils.getRealmName(realmNumber);
+ realm = identitySession.getRealm(realmId);
+ if (realm == null) {
+ throw new IllegalStateException("Realm '" + realmId + "' not found");
+ }
+
+ log.info("Read setup: realmsOffset=" + realmsOffset);
+ }
+
+ @Override
+ public void run(SampleResult result, KeycloakSession identitySession) {
+ throw new IllegalStateException("Not yet supported");
+ /*
+ int userNumber = ++userCounterInRealm;
+ int totalUserNumber = totalUserCounter.incrementAndGet();
+
+ String username = PerfTestUtils.getUsername(userNumber);
+
+ // TODO: Not supported in model actually. We support operation just in MongoDB
+ // UserModel user = realm.removeUser(username);
+ if (PropertiesManager.isMongoSessionFactory()) {
+ RealmAdapter mongoRealm = (RealmAdapter)realm;
+ mongoRealm.removeUser(username);
+ } else {
+ throw new IllegalArgumentException("Actually removing of users is supported just for MongoDB");
+ }
+
+ log.info("Finished removing of user " + username + " in realm: " + realm.getId());
+
+ int labelC = ((totalUserNumber - 1) / NUMBER_OF_USERS_IN_EACH_REPORT) * NUMBER_OF_USERS_IN_EACH_REPORT;
+ result.setSampleLabel("ReadUsers " + (labelC + 1) + "-" + (labelC + NUMBER_OF_USERS_IN_EACH_REPORT));
+ */
+ }
+
+ @Override
+ public void tearDown() {
+ }
+}
diff --git a/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/Worker.java b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/Worker.java
new file mode 100644
index 0000000..69732cf
--- /dev/null
+++ b/testsuite/performance/src/test/java/org/keycloak/testsuite/performance/Worker.java
@@ -0,0 +1,17 @@
+package org.keycloak.testsuite.performance;
+
+import org.apache.jmeter.samplers.SampleResult;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface Worker {
+
+ void setup(int workerId, KeycloakSession identitySession);
+
+ void run(SampleResult result, KeycloakSession identitySession);
+
+ void tearDown();
+
+}
diff --git a/testsuite/performance/src/test/jmeter/jmeter.properties b/testsuite/performance/src/test/jmeter/jmeter.properties
new file mode 100644
index 0000000..39b6ce4
--- /dev/null
+++ b/testsuite/performance/src/test/jmeter/jmeter.properties
@@ -0,0 +1,20 @@
+#Thu Mar 07 18:46:04 BRT 2013
+not_in_menu=HTML Parameter Mask,HTTP User Parameter Modifier
+xml.parser=org.apache.xerces.parsers.SAXParser
+cookies=cookies
+wmlParser.className=org.apache.jmeter.protocol.http.parser.RegexpHTMLParser
+HTTPResponse.parsers=htmlParser wmlParser
+remote_hosts=127.0.0.1
+system.properties=system.properties
+beanshell.server.file=../extras/startup.bsh
+log_level.jmeter.junit=DEBUG
+sampleresult.timestamp.start=true
+jmeter.laf.mac=System
+log_level.jorphan=INFO
+classfinder.functions.contain=.functions.
+user.properties=user.properties
+wmlParser.types=text/vnd.wap.wml
+log_level.jmeter=DEBUG
+classfinder.functions.notContain=.gui.
+htmlParser.types=text/html application/xhtml+xml application/xml text/xml
+upgrade_properties=/bin/upgrade.properties
\ No newline at end of file
diff --git a/testsuite/performance/src/test/jmeter/keycloak_perf_test.jmx b/testsuite/performance/src/test/jmeter/keycloak_perf_test.jmx
new file mode 100644
index 0000000..b96dcf3
--- /dev/null
+++ b/testsuite/performance/src/test/jmeter/keycloak_perf_test.jmx
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="2.4" jmeter="2.9 r1437961">
+ <hashTree>
+ <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
+ <stringProp name="TestPlan.comments"></stringProp>
+ <boolProp name="TestPlan.functional_mode">false</boolProp>
+ <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+ <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+ <collectionProp name="Arguments.arguments"/>
+ </elementProp>
+ <stringProp name="TestPlan.user_define_classpath"></stringProp>
+ </TestPlan>
+ <hashTree>
+ <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
+ <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+ <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+ <boolProp name="LoopController.continue_forever">false</boolProp>
+ <stringProp name="LoopController.loops">10</stringProp>
+ </elementProp>
+ <stringProp name="ThreadGroup.num_threads">1</stringProp>
+ <stringProp name="ThreadGroup.ramp_time">0</stringProp>
+ <longProp name="ThreadGroup.start_time">1362689985000</longProp>
+ <longProp name="ThreadGroup.end_time">1362689985000</longProp>
+ <boolProp name="ThreadGroup.scheduler">false</boolProp>
+ <stringProp name="ThreadGroup.duration"></stringProp>
+ <stringProp name="ThreadGroup.delay"></stringProp>
+ </ThreadGroup>
+ <hashTree>
+ <JavaSampler guiclass="JavaTestSamplerGui" testclass="JavaSampler" testname="Java Request" enabled="true">
+ <elementProp name="arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true">
+ <collectionProp name="Arguments.arguments">
+ </collectionProp>
+ </elementProp>
+ <stringProp name="classname">org.keycloak.testsuite.performance.BaseJMeterPerformanceTest</stringProp>
+ </JavaSampler>
+ </hashTree>
+ </hashTree>
+ </hashTree>
+</jmeterTestPlan>
\ No newline at end of file
diff --git a/testsuite/performance/src/test/jmeter/system.properties b/testsuite/performance/src/test/jmeter/system.properties
new file mode 100644
index 0000000..26d24aa
--- /dev/null
+++ b/testsuite/performance/src/test/jmeter/system.properties
@@ -0,0 +1,79 @@
+## Choose implementation of KeycloakSessionFactory
+# keycloak.sessionFactory=picketlink
+keycloak.sessionFactory=mongo
+
+## Configure JPA (just hbm2ddl schema configurable here. Rest of the stuff in META-INF/persistence.xml)
+keycloak.jpa.hbm2ddl.auto=create
+# keycloak.jpa.hbm2ddl.auto=update
+
+
+## Configure MongoDB (Useful just when keycloak.sessionFactory=mongo)
+keycloak.mongodb.host=localhost
+keycloak.mongodb.port=27017
+keycloak.mongodb.databaseName=keycloakPerfTest
+# Should be DB dropped at startup of the test?
+keycloak.mongodb.dropDatabaseOnStartup=true
+
+
+## Specify Keycloak worker class
+keycloak.perf.workerClass=org.keycloak.testsuite.performance.CreateRealmsWorker
+# keycloak.perf.workerClass=org.keycloak.testsuite.performance.CreateUsersWorker
+# keycloak.perf.workerClass=org.keycloak.testsuite.performance.ReadUsersWorker
+# keycloak.perf.workerClass=org.keycloak.testsuite.performance.RemoveUsersWorker
+
+
+## Properties for CreateRealms test. This test is used to create some realms.
+# Each iteration of single worker thread will add one realm and it will add some roles, defaultRoles, credentials and applications to it
+# Offset where to start creating realms. Count (total number of realms to create) is configurable as number of JMeter threads*loopCount
+# For example: if offset==1 and in JMeter properties we have LoopController.loops=10 and num_threads=2 then we will create 20 realms in total and we will create realms "realm1" - "realm10"
+# NOTE: Count (total number of realms to create) is configurable as number of JMeter threads*loopCount
+keycloak.perf.createRealms.realms.offset=1
+# Count of apps per each realm (For example if count=5, we will create apps like "realm1app1" - "realm1app5" for realm "realm1"
+# and similarly for all other created realms)
+keycloak.perf.createRealms.appsPerRealm=5
+# Count of roles per each realm (For example if count=5, we will create roles like "realm1role1" - "realm1role5" for realm "realm1"
+# and similarly for all other created realms)
+keycloak.perf.createRealms.rolesPerRealm=5
+# Count of default roles per each realm (For example if count=2, we will create roles like "realm1defrole1" and "realm1defrole2"
+# for realm "realm1" and similarly for all other created realms)
+keycloak.perf.createRealms.defaultRolesPerRealm=2
+# Count of roles per each application (For example if count=3 we will have roles "realm1app1role1" - "realm1app1role3" for realm=1 and application=1
+# (if realmsCount=10, appsPerRealm=5 it will be 150 application roles totally)
+keycloak.perf.createRealms.rolesPerApp=3
+# Whether to create required credentials in each realm (If true, we will create "password", "totp" and client-certificate)
+keycloak.perf.createRealms.createRequiredCredentials=true
+
+
+## Properties for CreateUsers test. This test is used to create some users
+# Each iteration of single worker thread will add one user and it will add some default roles, passwords and bind him with some social accounts
+# Each worker will use separate realm dedicated just for him, so each worker will create user1, user2, ... , userN . N (number of users to create per realm)
+# is configurable in JMeter configuration as loopCount. Total number of created users for whole test will be threads*loopCount
+# NOTE: For each thread, the corresponding realm must already exists
+# Realm where to start creating users
+keycloak.perf.createUsers.realms.offset=1
+# Whether to add basic attributes like firstName/lastName/email to each user
+keycloak.perf.createUsers.addBasicUserAttributes=true
+# Whether to add all default roles of realm to this user
+keycloak.perf.createUsers.addDefaultRoles=true
+# Whether to add password to this user
+keycloak.perf.createUsers.addPassword=true
+# Number of social links to create for each user. Possible values are 0, 1, 2, 3 (For 3 it will create Facebook, Twitter and Google)
+keycloak.perf.createUsers.socialLinksPerUserCount=0
+
+
+## Properties for ReadUsers test. This test is used to read some users from DB and alternatively read some of his properties (passwords, roles, scopes, socialLinks)
+keycloak.perf.readUsers.realms.offset=1
+# Number of read users in each iteration
+keycloak.perf.readUsers.readUsersPerIteration=5
+# Number of users to read in each realm. After reading all 2000 users, reading will start again from user1
+keycloak.perf.readUsers.countOfUsersPerRealm=2000
+keycloak.perf.readUsers.readRoles=true
+keycloak.perf.readUsers.readScopes=true
+keycloak.perf.readUsers.readPassword=true
+keycloak.perf.readUsers.readSocialLinks=false
+keycloak.perf.readUsers.searchBySocialLinks=false
+
+
+## Properties for RemoveUsers worker. This test is used to remove some users from DB (and all their stuff actually)
+# Similarly like in CreateUsers test, each worker works just with one realm. Number of removed users depends on JMeter property loopCount
+keycloak.perf.removeUsers.realms.offset=1
diff --git a/testsuite/performance/src/test/resources/META-INF/persistence.xml b/testsuite/performance/src/test/resources/META-INF/persistence.xml
new file mode 100644
index 0000000..1dff641
--- /dev/null
+++ b/testsuite/performance/src/test/resources/META-INF/persistence.xml
@@ -0,0 +1,40 @@
+<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="keycloak-identity-store" transaction-type="RESOURCE_LOCAL">
+ <provider>org.hibernate.ejb.HibernatePersistence</provider>
+
+ <class>org.picketlink.idm.jpa.model.sample.simple.AttributedTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.AccountTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.RoleTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.GroupTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.IdentityTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.RelationshipTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.RelationshipIdentityTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.PartitionTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.PasswordCredentialTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.DigestCredentialTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.X509CredentialTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.OTPCredentialTypeEntity</class>
+ <class>org.picketlink.idm.jpa.model.sample.simple.AttributeTypeEntity</class>
+ <class>org.keycloak.services.models.picketlink.mappings.RealmEntity</class>
+ <class>org.keycloak.services.models.picketlink.mappings.ApplicationEntity</class>
+
+ <exclude-unlisted-classes>true</exclude-unlisted-classes>
+
+ <properties>
+ <property name="hibernate.connection.url" value="jdbc:mysql://localhost/keycloakPerfTest"/>
+ <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/>
+ <property name="hibernate.connection.username" value="portal"/>
+ <property name="hibernate.connection.password" value="portal"/>
+ <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
+ <!--<property name="hibernate.hbm2ddl.auto" value="update" />-->
+ <property name="hibernate.hbm2ddl.auto" value="${keycloak.jpa.hbm2ddl.auto}" />
+ <property name="hibernate.show_sql" value="false" />
+ <property name="hibernate.format_sql" value="true" />
+ </properties>
+ </persistence-unit>
+
+
+</persistence>
\ No newline at end of file
testsuite/pom.xml 217(+7 -210)
diff --git a/testsuite/pom.xml b/testsuite/pom.xml
index 7c5278d..c8b7bcf 100755
--- a/testsuite/pom.xml
+++ b/testsuite/pom.xml
@@ -8,216 +8,13 @@
</parent>
<modelVersion>4.0.0</modelVersion>
- <artifactId>keycloak-testsuite</artifactId>
- <name>Keycloak TestSuite</name>
+ <artifactId>keycloak-testsuite-pom</artifactId>
+ <packaging>pom</packaging>
+ <name>Keycloak TestSuite</name>
<description />
+ <modules>
+ <module>integration</module>
+ <module>performance</module>
+ </modules>
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-as7-adapter</artifactId>
- <version>${project.version}</version>
- </dependency>
- </dependencies>
- </dependencyManagement>
-
- <dependencies>
- <dependency>
- <groupId>org.bouncycastle</groupId>
- <artifactId>bcprov-jdk16</artifactId>
- </dependency>
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-core</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-services</artifactId>
- <version>${project.version}</version>
- </dependency>
- <!--
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-server</artifactId>
- <version>${project.version}</version>
- </dependency>
- -->
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-social-core</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.keycloak</groupId>
- <artifactId>keycloak-social-facebook</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-forms</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.jboss.logging</groupId>
- <artifactId>jboss-logging</artifactId>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-idm-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-common</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>
- <dependency>
- <groupId>org.picketlink</groupId>
- <artifactId>picketlink-config</artifactId>
- </dependency>
- <dependency>
- <groupId>org.jboss.resteasy</groupId>
- <artifactId>resteasy-jaxrs</artifactId>
- <exclusions>
- <exclusion>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-simple</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.jboss.resteasy</groupId>
- <artifactId>jaxrs-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.jboss.resteasy</groupId>
- <artifactId>resteasy-client</artifactId>
- </dependency>
- <dependency>
- <groupId>org.jboss.resteasy</groupId>
- <artifactId>resteasy-crypto</artifactId>
- </dependency>
- <dependency>
- <groupId>org.jboss.resteasy</groupId>
- <artifactId>jose-jwt</artifactId>
- </dependency>
- <dependency>
- <groupId>org.jboss.resteasy</groupId>
- <artifactId>resteasy-undertow</artifactId>
- </dependency>
- <dependency>
- <groupId>io.undertow</groupId>
- <artifactId>undertow-servlet</artifactId>
- </dependency>
- <dependency>
- <groupId>io.undertow</groupId>
- <artifactId>undertow-core</artifactId>
- </dependency>
- <dependency>
- <groupId>org.codehaus.jackson</groupId>
- <artifactId>jackson-core-asl</artifactId>
- </dependency>
- <dependency>
- <groupId>org.codehaus.jackson</groupId>
- <artifactId>jackson-mapper-asl</artifactId>
- </dependency>
- <dependency>
- <groupId>org.jboss.spec.javax.servlet</groupId>
- <artifactId>jboss-servlet-api_3.0_spec</artifactId>
- </dependency>
- <dependency>
- <groupId>org.codehaus.jackson</groupId>
- <artifactId>jackson-xc</artifactId>
- </dependency>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- </dependency>
- <dependency>
- <groupId>org.hibernate.javax.persistence</groupId>
- <artifactId>hibernate-jpa-2.0-api</artifactId>
- </dependency>
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <version>1.3.161</version>
- </dependency>
- <dependency>
- <groupId>org.hibernate</groupId>
- <artifactId>hibernate-entitymanager</artifactId>
- <version>3.6.6.Final</version>
- </dependency>
- <dependency>
- <groupId>com.icegreen</groupId>
- <artifactId>greenmail</artifactId>
- </dependency>
- <dependency>
- <groupId>org.seleniumhq.selenium</groupId>
- <artifactId>selenium-java</artifactId>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <source>1.6</source>
- <target>1.6</target>
- </configuration>
- </plugin>
- </plugins>
- </build>
-
- <profiles>
- <profile>
- <id>jboss-managed</id>
- <dependencies>
- <dependency>
- <groupId>org.jboss.as</groupId>
- <artifactId>jboss-as-arquillian-container-managed</artifactId>
- <scope>test</scope>
- <version>7.1.1.Final</version>
- </dependency>
- </dependencies>
- </profile>
- <profile>
- <id>jboss-remote</id>
- <dependencies>
- <dependency>
- <groupId>org.jboss.as</groupId>
- <artifactId>jboss-as-arquillian-container-remote</artifactId>
- <scope>test</scope>
- <version>7.1.1.Final</version>
- </dependency>
- </dependencies>
- </profile>
- </profiles>
</project>