diff --git a/examples/as7-eap-demo/server/pom.xml b/examples/as7-eap-demo/server/pom.xml index 7027dc52cb..3282368dd4 100755 --- a/examples/as7-eap-demo/server/pom.xml +++ b/examples/as7-eap-demo/server/pom.xml @@ -118,7 +118,14 @@ com.h2database h2 - 1.3.161 + + + org.mongodb + mongo-java-driver + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo junit 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 6829b0e56e..fafd74452f 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 @@ true + + org.keycloak.services.listeners.MongoRunnerListener + + Keycloak Session Management org.keycloak.services.filters.KeycloakSessionServletFilter diff --git a/forms/pom.xml b/forms/pom.xml index 6d0dec8ebf..0af1892e05 100755 --- a/forms/pom.xml +++ b/forms/pom.xml @@ -46,12 +46,6 @@ org.jboss.spec.javax.servlet jboss-servlet-api_3.0_spec - 1.0.2.Final - - - javax.faces - jsf-api - 2.1 com.google.zxing 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 0000000000..9119282b4f --- /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 Marek Posolda + */ +public class KeycloakSessionUtils { + + private static AtomicLong counter = new AtomicLong(1); + + public static String generateId() { + return counter.getAndIncrement() + "-" + System.currentTimeMillis(); + } +} diff --git a/model/mongo/pom.xml b/model/mongo/pom.xml new file mode 100644 index 0000000000..537112a618 --- /dev/null +++ b/model/mongo/pom.xml @@ -0,0 +1,77 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-1 + ../../pom.xml + + 4.0.0 + + keycloak-model-mongo + Keycloak Model Mongo + + + + + org.bouncycastle + bcprov-jdk16 + provided + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-model-api + ${project.version} + + + org.jboss.logging + jboss-logging + provided + + + org.picketlink + picketlink-common + provided + + + org.picketlink + picketlink-idm-api + provided + + + org.mongodb + mongo-java-driver + provided + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + \ 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 0000000000..81546ba469 --- /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 Marek Posolda + */ +public abstract class AbstractAttributedNoSQLObject extends AbstractNoSQLObject implements AttributedNoSQLObject { + + // Simple hashMap for now (no thread-safe) + private Map attributes = new HashMap(); + + @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 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 0000000000..837e5e4644 --- /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 Marek Posolda + */ +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 0000000000..45accd1c1c --- /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 Marek Posolda + */ +public interface AttributedNoSQLObject extends NoSQLObject { + + void setAttribute(String name, String value); + + void removeAttribute(String name); + + String getAttribute(String name); + + Map 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 0000000000..3bc62a590f --- /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 Marek Posolda + */ +public interface NoSQL { + + /** + * Insert object if it's oid is null. Otherwise update + */ + void saveObject(NoSQLObject object); + + T loadObject(Class type, String oid); + + T loadSingleObject(Class type, NoSQLQuery query); + + List loadObjects(Class type, NoSQLQuery query); + + // Object must have filled oid + void removeObject(NoSQLObject object); + + void removeObject(Class type, String oid); + + void removeObjects(Class type, NoSQLQuery query); + + NoSQLQueryBuilder createQueryBuilder(); + + void pushItemToList(NoSQLObject object, String listPropertyName, S itemToPush); + + 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 0000000000..80b63326f0 --- /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 Marek Posolda + */ +@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 0000000000..3af69a7135 --- /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 Marek Posolda + */ +@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 0000000000..06ed01e655 --- /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 Marek Posolda + */ +@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 0000000000..0242243936 --- /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 Marek Posolda + */ +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 0000000000..29cc0f31ab --- /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 Marek Posolda + */ +public class NoSQLQuery { + + private final Map queryAttributes; + + NoSQLQuery(Map queryAttributes) { + this.queryAttributes = queryAttributes; + }; + + public Map 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 0000000000..dcdb5752a3 --- /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 Marek Posolda + */ +public abstract class NoSQLQueryBuilder { + + private Map queryAttributes = new HashMap(); + + 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 0000000000..a6b6c869e6 --- /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 Marek Posolda + */ +public interface Converter { + + S convertObject(T objectToConvert); + + Class getConverterObjectType(); + + Class 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 0000000000..a7c12c0cfb --- /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 Marek Posolda + */ +public class TypeConverter { + + // TODO: Thread-safety support (maybe...) + // Converters of Application objects to DB objects + private Map, Converter> appObjectConverters = new HashMap, Converter>(); + + // Converters of DB objects to Application objects + private Map, Map, Converter>> dbObjectConverters = new HashMap, Map, 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, Converter> appObjects = dbObjectConverters.get(dbObjectType); + if (appObjects == null) { + appObjects = new HashMap, Converter>(); + dbObjectConverters.put(dbObjectType, appObjects); + } + appObjects.put(appObjectType, converter); + } + + + public S convertDBObjectToApplicationObject(Object dbObject, Class expectedApplicationObjectType) { + Class dbObjectType = dbObject.getClass(); + Converter converter; + + Map, 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)appObjects.values().iterator().next(); + } else { + // Try to find converter for requested application type + converter = (Converter)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 convertApplicationObjectToDBObject(Object applicationObject, Class expectedDBObjectType) { + Class appObjectType = applicationObject.getClass(); + Converter converter = (Converter)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 getAppConverterForType(Class appObjectType, Map, Converter> appObjectConverters) { + Converter converter = (Converter)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 0000000000..09ca78e378 --- /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 Marek Posolda + */ +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, ObjectInfo> objectInfoCache = + new ConcurrentHashMap, ObjectInfo>(); + + + public MongoDBImpl(DB database, boolean dropDatabaseOnStartup, Class[] 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 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 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 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 loadObject(Class 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 loadSingleObject(Class type, NoSQLQuery query) { + List 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 List loadObjects(Class 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 type = object.getClass(); + ObjectInfo objectInfo = getObjectInfo(type); + + Property idProperty = objectInfo.getOidProperty(); + String oid = idProperty.getValue(object); + + removeObject(type, oid); + } + + + @Override + public void removeObject(Class 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 type, NoSQLQuery query) { + List 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 void pushItemToList(NoSQLObject object, String listPropertyName, S itemToPush) { + Class type = object.getClass(); + ObjectInfo objectInfo = getObjectInfo(type); + + Property 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 listProperty = objectInfo.getPropertyByName(listPropertyName); + if (listProperty == null) { + throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + object); + } + + List list = (List)listProperty.getValue(object); + if (list == null) { + list = new ArrayList(); + 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 void pullItemFromList(NoSQLObject object, String listPropertyName, S itemToPull) { + Class type = object.getClass(); + ObjectInfo objectInfo = getObjectInfo(type); + + Property 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 listProperty = objectInfo.getPropertyByName(listPropertyName); + if (listProperty == null) { + throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + object); + } + List list = (List)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 objectClass) { + ObjectInfo objectInfo = objectInfoCache.get(objectClass); + if (objectInfo == null) { + Property idProperty = PropertyQueries.createQuery(objectClass).addCriteria(new AnnotatedPropertyCriteria(NoSQLId.class)).getFirstResult(); + + List> 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 List convertCursor(Class type, DBCursor cursor) { + List result = new ArrayList(); + + try { + for (DBObject dbObject : cursor) { + T converted = typeConverter.convertDBObjectToApplicationObject(dbObject, type); + result.add(converted); + } + } finally { + cursor.close(); + } + + return result; + } + + private DBCollection getDBCollectionForType(Class type) { + ObjectInfo objectInfo = getObjectInfo(type); + return database.getCollection(objectInfo.getDbCollectionName()); + } + + private BasicDBObject getDBQueryFromQuery(NoSQLQuery query) { + Map queryAttributes = query.getQueryAttributes(); + BasicDBObject dbQuery = new BasicDBObject(); + for (Map.Entry 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 0000000000..f56c799aea --- /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 Marek Posolda + */ +public class MongoDBQueryBuilder extends NoSQLQueryBuilder { + + protected MongoDBQueryBuilder() {}; + + @Override + public NoSQLQueryBuilder inCondition(String name, List values) { + if (values == null) { + values = new LinkedList(); + } + + if ("_id".equals(name)) { + // we need to convert Strings to ObjectID + List objIds = new ArrayList(); + 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 0000000000..ae548a607a --- /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 Marek Posolda + */ +public class ObjectInfo { + + private final Class objectClass; + + private final String dbCollectionName; + + private final Property oidProperty; + + private final Map> properties; + + public ObjectInfo(Class objectClass, String dbCollectionName, Property oidProperty, List> properties) { + this.objectClass = objectClass; + this.dbCollectionName = dbCollectionName; + this.oidProperty = oidProperty; + + Map> props= new HashMap>(); + for (Property property : properties) { + props.put(property.getName(), property); + } + this.properties = Collections.unmodifiableMap(props); + } + + public Class getObjectClass() { + return objectClass; + } + + public String getDbCollectionName() { + return dbCollectionName; + } + + public Property getOidProperty() { + return oidProperty; + } + + public Collection> getProperties() { + return properties.values(); + } + + public Property 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 0000000000..896257fa8b --- /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 Marek Posolda + */ +public class BasicDBListConverter implements Converter { + + private final TypeConverter typeConverter; + + public BasicDBListConverter(TypeConverter typeConverter) { + this.typeConverter = typeConverter; + } + + @Override + public ArrayList convertObject(BasicDBList dbList) { + ArrayList appObjects = new ArrayList(); + Class expectedListElementType = null; + for (Object dbObject : dbList) { + + if (expectedListElementType == null) { + expectedListElementType = findExpectedListElementType(dbObject); + } + + appObjects.add(typeConverter.convertDBObjectToApplicationObject(dbObject, expectedListElementType)); + } + return appObjects; + } + + @Override + public Class getConverterObjectType() { + return BasicDBList.class; + } + + @Override + public Class 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 0000000000..a423652b38 --- /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 Marek Posolda + */ +public class BasicDBObjectConverter implements Converter { + + private static final Logger logger = Logger.getLogger(BasicDBObjectConverter.class); + + private final MongoDBImpl mongoDBImpl; + private final TypeConverter typeConverter; + private final Class expectedNoSQLObjectType; + + public BasicDBObjectConverter(MongoDBImpl mongoDBImpl, TypeConverter typeConverter, Class 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 property; + + if ("_id".equals(key)) { + // Current property is "id" + Property 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 getConverterObjectType() { + return BasicDBObject.class; + } + + @Override + public Class 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 0000000000..891ccdd0e5 --- /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 Marek Posolda + */ +public class ClassCache { + + public static final String SPLIT = "###"; + private static final ClassCache INSTANCE = new ClassCache(); + + private ConcurrentMap> cache = new ConcurrentHashMap>(); + + 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 0000000000..2a800df46b --- /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 Marek Posolda + */ +public class EnumToStringConverter implements Converter { + + // 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 getConverterObjectType() { + return Enum.class; + } + + @Override + public Class 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 0000000000..8b72ca223c --- /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 Marek Posolda + */ +public class ListConverter implements Converter { + + // 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 listType; + + public ListConverter(TypeConverter typeConverter, Class 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 getConverterObjectType() { + return listType; + } + + @Override + public Class 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 0000000000..f7be7aea33 --- /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 Marek Posolda + */ +public class NoSQLObjectConverter implements Converter { + + private final MongoDBImpl mongoDBImpl; + private final TypeConverter typeConverter; + private final Class expectedNoSQLObjectType; + + public NoSQLObjectConverter(MongoDBImpl mongoDBImpl, TypeConverter typeConverter, Class 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> props = objectInfo.getProperties(); + for (Property 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 attributes = attributedObject.getAttributes(); + for (Map.Entry attribute : attributes.entrySet()) { + dbObject.append(attribute.getKey(), attribute.getValue()); + } + } + + return dbObject; + } + + @Override + public Class getConverterObjectType() { + return expectedNoSQLObjectType; + } + + @Override + public Class 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 0000000000..5ba1de5498 --- /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 Marek Posolda + */ +public class SimpleConverter implements Converter { + + private final Class expectedType; + + public SimpleConverter(Class expectedType) { + this.expectedType = expectedType; + } + + @Override + public T convertObject(T objectToConvert) { + return objectToConvert; + } + + @Override + public Class getConverterObjectType() { + return expectedType; + } + + @Override + public Class 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 0000000000..0c948eccb4 --- /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 Marek Posolda + */ +public class StringToEnumConverter implements Converter { + + @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 clazz = (Class)ClassCache.getInstance().getOrLoadClass(className); + return Enum.valueOf(clazz, enumValue); + } + + @Override + public Class getConverterObjectType() { + return String.class; + } + + @Override + public Class 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 0000000000..49bcd31b88 --- /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 Marek Posolda + */ +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 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 getRoles() { + NoSQLQuery query = noSQL.createQueryBuilder() + .andCondition("applicationId", getId()) + .build(); + List roles = noSQL.loadObjects(RoleData.class, query); + + List result = new ArrayList(); + 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 getAllRolesOfUser(UserModel user, NoSQL noSQL) { + UserData userData = ((UserAdapter)user).getUser(); + List roleIds = userData.getRoleIds(); + + NoSQLQuery query = noSQL.createQueryBuilder() + .inCondition("_id", roleIds) + .build(); + return noSQL.loadObjects(RoleData.class, query); + } + + @Override + public Set getRoleMappingValues(UserModel user) { + Set result = new HashSet(); + List 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 getRoleMappings(UserModel user) { + List result = new ArrayList(); + List 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 getAllScopesOfUser(UserModel user, NoSQL noSQL) { + UserData userData = ((UserAdapter)user).getUser(); + List roleIds = userData.getScopeIds(); + + NoSQLQuery query = noSQL.createQueryBuilder() + .inCondition("_id", roleIds) + .build(); + return noSQL.loadObjects(RoleData.class, query); + } + + @Override + public Set getScopeMappingValues(UserModel agent) { + Set result = new HashSet(); + List 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 getScopeMappings(UserModel agent) { + List result = new ArrayList(); + List 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 0000000000..b1ac5093ec --- /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 Marek Posolda + */ +public class MongoDBSessionFactory implements KeycloakSessionFactory { + protected static final Logger logger = Logger.getLogger(MongoDBSessionFactory.class); + + private static final Class[] MANAGED_DATA_TYPES = (Class[])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 0000000000..2bc413de5f --- /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 Marek Posolda + */ +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 getRealms(UserModel admin) { + String userId = ((UserAdapter)admin).getUser().getId(); + NoSQLQuery query = noSQL.createQueryBuilder() + .andCondition("realmAdmins", userId) + .build(); + List realms = noSQL.loadObjects(RealmData.class, query); + + List results = new ArrayList(); + 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 0000000000..3d166357ae --- /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 Marek Posolda + */ +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 0000000000..34f455eb39 --- /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 Marek Posolda + */ +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 0000000000..837f985624 --- /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 Marek Posolda + */ +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()); + private static TOTPCredentialHandler totpCredentialHandler = new TOTPCredentialHandler(new HashMap()); + + 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 getRoles() { + NoSQLQuery query = noSQL.createQueryBuilder() + .andCondition("realmId", getOid()) + .build(); + List roles = noSQL.loadObjects(RoleData.class, query); + + List result = new ArrayList(); + for (RoleData role : roles) { + result.add(new RoleAdapter(role, noSQL)); + } + + return result; + } + + @Override + public List getDefaultRoles() { + List defaultRoles = realm.getDefaultRoles(); + + NoSQLQuery query = noSQL.createQueryBuilder() + .inCondition("_id", defaultRoles) + .build(); + List defaultRolesData = noSQL.loadObjects(RoleData.class, query); + + List defaultRoleModels = new ArrayList(); + 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 roleIds = new ArrayList(); + 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 getApplicationNameMap() { + Map resourceMap = new HashMap(); + for (ApplicationModel resource : getApplications()) { + resourceMap.put(resource.getName(), resource); + } + return resourceMap; + } + + @Override + public List getApplications() { + NoSQLQuery query = noSQL.createQueryBuilder() + .andCondition("realmId", getOid()) + .build(); + List appDatas = noSQL.loadObjects(ApplicationData.class, query); + + List result = new ArrayList(); + 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 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 getRoleMappings(UserModel user) { + List result = new ArrayList(); + List 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 getRoleMappingValues(UserModel user) { + Set result = new HashSet(); + List 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 getOAuthClients() { + NoSQLQuery query = noSQL.createQueryBuilder() + .andCondition("realmId", getOid()) + .build(); + List results = noSQL.loadObjects(OAuthClientData.class, query); + List list = new ArrayList(); + for (OAuthClientData data : results) { + list.add(new OAuthClientAdapter(data, noSQL)); + } + return list; + } + + @Override + public List getScopeMappings(UserModel agent) { + List result = new ArrayList(); + List 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 getScopeMappingValues(UserModel agent) { + Set result = new HashSet(); + List 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 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 creds) { + List credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_USER); + updateRequiredCredentials(creds, credsData); + } + + @Override + public void updateRequiredApplicationCredentials(Set creds) { + List credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_RESOURCE); + updateRequiredCredentials(creds, credsData); + } + + @Override + public void updateRequiredOAuthClientCredentials(Set creds) { + List credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE); + updateRequiredCredentials(creds, credsData); + } + + protected void updateRequiredCredentials(Set creds, List credsData) { + Set already = new HashSet(); + 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 getRequiredCredentials() { + return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_USER); + } + + @Override + public List getRequiredApplicationCredentials() { + return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_RESOURCE); + } + + @Override + public List getRequiredOAuthClientCredentials() { + return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE); + } + + protected List getRequiredCredentials(int credentialType) { + List credsData = getRequiredCredentialsData(credentialType); + + List result = new ArrayList(); + 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 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 getSocialLinks(UserModel user) { + UserData userData = ((UserAdapter)user).getUser(); + String userId = userData.getId(); + + NoSQLQuery query = noSQL.createQueryBuilder() + .andCondition("userId", userId) + .build(); + List dbSocialLinks = noSQL.loadObjects(SocialLinkData.class, query); + + Set result = new HashSet(); + 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 searchForUserByAttributes(Map attributes) { + NoSQLQueryBuilder queryBuilder = noSQL.createQueryBuilder(); + for (Map.Entry 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 users = noSQL.loadObjects(UserData.class, queryBuilder.build()); + List userModels = new ArrayList(); + 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 0000000000..7b2692f42c --- /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 Marek Posolda + */ +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 0000000000..c047361f31 --- /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 Marek Posolda + */ +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 getAttributes() { + return user.getAttributes(); + } + + public UserData getUser() { + return user; + } + + @Override + public Set getRequiredActions() { + List actions = user.getRequiredActions(); + + // Compatibility with picketlink impl + if (actions == null) { + return Collections.emptySet(); + } else { + Set s = new HashSet(); + 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 0000000000..719760a0f5 --- /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 Marek Posolda + */ +public class PasswordCredentialHandler { + + private static final String DEFAULT_SALT_ALGORITHM = "SHA1PRNG"; + + /** + *

+ * Stores a stateless instance of {@link org.picketlink.idm.credential.encoder.PasswordEncoder} that should be used to encode passwords. + *

+ */ + public static final String PASSWORD_ENCODER = "PASSWORD_ENCODER"; + + private PasswordEncoder passwordEncoder = new SHAPasswordEncoder(512);; + + public PasswordCredentialHandler(Map options) { + setup(options); + } + + private void setup(Map 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); + } + + /** + *

+ * Salt the give rawPassword with the specified salt value. + *

+ * + * @param rawPassword + * @param salt + * @return + */ + private String saltPassword(String rawPassword, String salt) { + return salt + rawPassword; + } + + /** + *

+ * Generates a random string to be used as a salt for passwords. + *

+ * + * @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 0000000000..b8f02e7d6e --- /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 Marek Posolda + */ +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 options) { + super(options); + setup(options); + } + + private void setup(Map 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 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 0000000000..5ceb788dbb --- /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 Marek Posolda + */ +@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/OAuthClientData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java new file mode 100644 index 0000000000..67f74ee6a3 --- /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 Marek Posolda + */ +@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 0000000000..5247d60034 --- /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 Marek Posolda + */ +@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 defaultRoles; + private List 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 getDefaultRoles() { + return defaultRoles; + } + + public void setDefaultRoles(List defaultRoles) { + this.defaultRoles = defaultRoles; + } + + @NoSQLField + public List getRealmAdmins() { + return realmAdmins; + } + + public void setRealmAdmins(List 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 0000000000..e46ee9fd07 --- /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 Marek Posolda + */ +@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 0000000000..29bc1f8930 --- /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 Marek Posolda + */ +@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 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 0000000000..37ea43d44e --- /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 Marek Posolda + */ +@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 0000000000..cfeb67d6d1 --- /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 Marek Posolda + */ +@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 roleIds; + private List scopeIds; + private List 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 getRoleIds() { + return roleIds; + } + + public void setRoleIds(List roleIds) { + this.roleIds = roleIds; + } + + @NoSQLField + public List getScopeIds() { + return scopeIds; + } + + public void setScopeIds(List scopeIds) { + this.scopeIds = scopeIds; + } + + @NoSQLField + public List getRequiredActions() { + return requiredActions; + } + + public void setRequiredActions(List 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 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/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 0000000000..8ab31a65fc --- /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 Marek Posolda + */ +@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 0000000000..7480e1fb87 --- /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 Marek Posolda + */ +@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/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 0000000000..8f6b6f885e --- /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 Marek Posolda + */ +public class Address extends AbstractNoSQLObject { + + private String street; + private int number; + private List 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 getFlatNumbers() { + return flatNumbers; + } + + public void setFlatNumbers(List 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 0000000000..262ade855b --- /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 Marek Posolda + */ +public class MongoDBModelTest { + + private static final Class[] MANAGED_DATA_TYPES = (Class[])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
addresses = new ArrayList
(); + 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 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 0000000000..ab2ded3844 --- /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 Marek Posolda + */ +@NoSQLCollection(collectionName = "persons") +public class Person extends AbstractNoSQLObject { + + private String id; + private String firstName; + private int age; + private List kids; + private List
addresses; + private Address mainAddress; + private Gender gender; + private List 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 getGenders() { + return genders; + } + + public void setGenders(List genders) { + this.genders = genders; + } + + @NoSQLField + public List getKids() { + return kids; + } + + public void setKids(List kids) { + this.kids = kids; + } + + @NoSQLField + public List
getAddresses() { + return addresses; + } + + public void setAddresses(List
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 4139e03ee7..8e2a75d460 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 6c62007bb5..d5412cee08 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 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 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 e9be9d467b..da8f04f01c 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("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("socialUsername", socialProviderUserId)); } + + @AttributeProperty + public String getRealm() { + return (String)getAttribute("realm").getValue(); + } + + public void setRealm(String realm) { + setAttribute(new Attribute("realm", realm)); + } } diff --git a/model/pom.xml b/model/pom.xml index c6a4a9ef90..7e2fca5b9b 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -37,5 +37,6 @@ api picketlink jpa + diff --git a/pom.xml b/pom.xml index d08e744eeb..61503c50f2 100755 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,14 @@ 3.0.4.Final 1.0.0.Beta12 2.5.0.Beta6 + 2.11.2 + 3.1.1.GA + 1.0.1.Final + 3.6.6.Final + 1.3.161 + 1.6.1 + 5.1.25 + 1.6.1 http://keycloak.org @@ -149,6 +157,11 @@ jboss-servlet-api_3.0_spec 1.0.1.Final + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.1_spec + 1.0.0.Beta1 + org.picketlink picketlink-common @@ -177,7 +190,7 @@ org.jboss.logging jboss-logging - 3.1.1.GA + ${jboss.logging.version} junit @@ -187,7 +200,17 @@ org.hibernate.javax.persistence hibernate-jpa-2.0-api - 1.0.1.Final + ${hibernate.javax.persistence.version} + + + com.h2database + h2 + ${h2.version} + + + org.hibernate + hibernate-entitymanager + ${hibernate.entitymanager.version} com.google.api-client @@ -236,9 +259,9 @@ com.icegreen greenmail 1.3.1b - + - + org.seleniumhq.selenium selenium-java @@ -248,7 +271,39 @@ org.seleniumhq.selenium selenium-chrome-driver 2.35.0 - + + + org.mongodb + mongo-java-driver + 2.11.2 + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + 1.27 + + + org.apache.jmeter + ApacheJMeter_java + 2.9 + + + dom4j + dom4j + ${dom4j.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + mysql + mysql-connector-java + ${mysql.version} + + @@ -330,6 +385,26 @@ false + + com.lazerycode.jmeter + jmeter-maven-plugin + 1.8.1 + + + com.lazerycode.jmeter + jmeter-analysis-maven-plugin + 1.0.4 + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + diff --git a/services/pom.xml b/services/pom.xml index 4d8075f8cc..187d7be695 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -34,6 +34,12 @@ keycloak-model-picketlink ${project.version} + + org.keycloak keycloak-social-core @@ -144,6 +150,21 @@ jackson-xc provided + + org.picketlink + picketlink-common + provided + + + org.mongodb + mongo-java-driver + provided + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + provided + junit junit @@ -157,13 +178,11 @@ com.h2database h2 - 1.3.161 test org.hibernate hibernate-entitymanager - 3.6.6.Final test 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 0000000000..f0df0a627f --- /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 Marek Posolda + */ +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/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 49855b9af7..03407519c7 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 singletons = new HashSet(); protected Set> classes = new HashSet>(); @@ -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 mongoDBSessionFactoryClass = (Class)Class.forName("org.keycloak.models.mongo.keycloak.adapters.MongoDBSessionFactory"); + Constructor 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/resources/flows/FormFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/FormFlows.java index cdb588e2e5..133b062354 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/utils/PropertiesManager.java b/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java new file mode 100644 index 0000000000..ee16547104 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java @@ -0,0 +1,82 @@ +package org.keycloak.services.utils; + +/** + * @author Marek Posolda + */ +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 ce3ed3d8c1..a2049fd8bd 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 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 1bdaa82254..41aa8e15b2 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 userModels = adapter.searchUsers("total junk query", realmModel); Assert.assertEquals(userModels.size(), 0); diff --git a/services/src/test/java/org/keycloak/test/ImportTest.java b/services/src/test/java/org/keycloak/test/ImportTest.java index d426d4e4d3..33348ed252 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; +public class ImportTest extends AbstractKeycloakTest { - @Before - public void before() throws Exception { - factory = KeycloakApplication.buildSessionFactory(); - identitySession = factory.createSession(); - identitySession.getTransaction().begin(); - manager = new RealmManager(identitySession); - } - - @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 resources = realm.getApplications(); Assert.assertEquals(2, resources.size()); - List realms = identitySession.getRealms(admin); + List 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); 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 0000000000..baaa9f665d --- /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 Marek Posolda + */ +@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 parameters() { + List params = new ArrayList(); + + 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 0000000000..def3f342c4 --- /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 Marek Posolda + */ +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 0000000000..80b2cbc31d --- /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 Marek Posolda + */ +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 0000000000..a35cfd25c6 --- /dev/null +++ b/services/src/test/java/org/keycloak/test/common/SessionFactoryTestContext.java @@ -0,0 +1,17 @@ +package org.keycloak.test.common; + +/** + * @author Marek Posolda + */ +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/testsuite/README.md b/testsuite/README.md deleted file mode 100644 index 00d5c0c85c..0000000000 --- a/testsuite/README.md +++ /dev/null @@ -1,54 +0,0 @@ -Executing testsuite -=================== - -Browser -------- - -The testsuite uses Sellenium. By default it uses the HtmlUnit WebDriver, but can also be executed with Chrome or Firefox. - -To run the tests with Firefox add `-Dbrowser=firefox` or for Chrome add `-Dbrowser=chrome` - - -Test utils -========== - -Keycloak server ---------------- - -To start a basic Keycloak server for testing run: - - mvn exec:java -Dexec.mainClass=org.keycloak.testutils.KeycloakServer - -or just run KeycloakServer from your favourite IDE! - -When starting the server it can also import a realm from a json file: - - mvn exec:java -Dexec.mainClass=org.keycloak.testutils.KeycloakServer -Dexec.args="-import testrealm.json" - -You can also change the host and port the server is bound to: - - mvn exec:java -Dexec.mainClass=org.keycloak.testutils.KeycloakServer -Dexec.args="-b host -p 8080" - -TOTP codes ----------- - -To generate totp codes without Google authenticator run: - - mvn exec:java -Dexec.mainClass=org.keycloak.testutils.TotpGenerator -Dexec.args="PJBX GURY NZIT C2JX I44T S3D2 JBKD G6SB" - -or just run TotpGenerator from your favourite IDE! - -Replace value of -Dexec.args with the secret from the totp configuration page - -Mail server ------------ - -To start a test mail server for testing email sending run: - - mvn exec:java -Dexec.mainClass=org.keycloak.testutils.MailServer - -or just run MailServer from your favourite IDE! - -To configure Keycloak to use the above server add: - - -Dkeycloak.mail.smtp.from=auto@keycloak.org -Dkeycloak.mail.smtp.host=localhost -Dkeycloak.mail.smtp.port=3025 diff --git a/testsuite/integration/README.md b/testsuite/integration/README.md new file mode 100644 index 0000000000..3145d38ee7 --- /dev/null +++ b/testsuite/integration/README.md @@ -0,0 +1,57 @@ +Executing testsuite +=================== + +Browser +------- + +The testsuite uses Sellenium. By default it uses the HtmlUnit WebDriver, but can also be executed with Chrome or Firefox. + +To run the tests with Firefox add `-Dbrowser=firefox` or for Chrome add `-Dbrowser=chrome` + + +Test utils +========== + +Keycloak server +--------------- + +To start a basic Keycloak server for testing run: + + mvn exec:java -Pkeycloak-server + +or run org.keycloak.testutils.KeycloakServer from your favourite IDE! + +When starting the server it can also import a realm from a json file: + + mvn exec:java -Pkeycloak-server -Dimport=testrealm.json + +TOTP codes +---------- + +To generate totp codes without Google authenticator run: + + mvn exec:java -Ptotp -Dsecret='PJBX GURY NZIT C2JX I44T S3D2 JBKD G6SB' + +or run org.keycloak.testutils.TotpGenerator from your favourite IDE! + +Replace value of -Dsecret with the secret from the totp configuration page (remember quotes!) + +Mail server +----------- + +To start a test mail server for testing email sending run: + + mvn exec:java -Pmail-server + +or run org.keycloak.testutils.MailServer from your favourite IDE! + +To configure Keycloak to use the above server add the following system properties: + + keycloak.mail.smtp.from=auto@keycloak.org + keycloak.mail.smtp.host=localhost + keycloak.mail.smtp.port=3025 + +For example if using the test utils Keycloak server start it with: + + mvn exec:java -Pkeycloak-server -Dkeycloak.mail.smtp.from=auto@keycloak.org -Dkeycloak.mail.smtp.host=localhost -Dkeycloak.mail.smtp.port=3025 + diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml new file mode 100644 index 0000000000..f0f3eeab35 --- /dev/null +++ b/testsuite/integration/pom.xml @@ -0,0 +1,253 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-1 + ../../pom.xml + + 4.0.0 + + keycloak-testsuite-integration + Keycloak Integration TestSuite + + + + + + org.keycloak + keycloak-as7-adapter + ${project.version} + + + + + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.1_spec + + + org.bouncycastle + bcprov-jdk16 + + + org.keycloak + keycloak-admin-ui + ${project.version} + + + org.keycloak + keycloak-admin-ui-styles + ${project.version} + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-services + ${project.version} + + + org.keycloak + keycloak-social-core + ${project.version} + + + org.keycloak + keycloak-social-google + ${project.version} + + + org.keycloak + keycloak-social-twitter + ${project.version} + + + org.keycloak + keycloak-social-facebook + ${project.version} + + + org.keycloak + keycloak-forms + ${project.version} + + + + org.jboss.logging + jboss-logging + + + org.picketlink + picketlink-idm-api + + + org.picketlink + picketlink-common + + + org.picketlink + picketlink-idm-impl + + + org.picketlink + picketlink-idm-simple-schema + + + org.picketlink + picketlink-config + + + org.jboss.resteasy + resteasy-jaxrs + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + + + org.jboss.resteasy + jaxrs-api + + + org.jboss.resteasy + resteasy-client + + + org.jboss.resteasy + resteasy-crypto + + + org.jboss.resteasy + jose-jwt + + + org.jboss.resteasy + resteasy-undertow + + + io.undertow + undertow-servlet + + + io.undertow + undertow-core + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.codehaus.jackson + jackson-xc + + + junit + junit + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + + + com.h2database + h2 + 1.3.161 + + + org.hibernate + hibernate-entitymanager + 3.6.6.Final + + + com.icegreen + greenmail + + + org.seleniumhq.selenium + selenium-java + + + org.mongodb + mongo-java-driver + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + + + keycloak-server + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.KeycloakServer + + + + + + + mail-server + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.MailServer + + + + + + + totp + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.TotpGenerator + + ${secret} + + + + + + + + \ No newline at end of file diff --git a/testsuite/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java similarity index 69% rename from testsuite/src/main/java/org/keycloak/testutils/KeycloakServer.java rename to testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java index e3d0afd8e7..6dd52b7ed8 100644 --- a/testsuite/src/main/java/org/keycloak/testutils/KeycloakServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java @@ -23,15 +23,13 @@ package org.keycloak.testutils; import io.undertow.Undertow; import io.undertow.Undertow.Builder; -import io.undertow.server.handlers.resource.ClassPathResourceManager; +import io.undertow.server.handlers.resource.*; import io.undertow.servlet.Servlets; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.FilterInfo; -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.net.URL; import javax.servlet.DispatcherType; @@ -46,7 +44,6 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.services.FormService; import org.keycloak.services.filters.KeycloakSessionServletFilter; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; @@ -64,6 +61,7 @@ public class KeycloakServer { public static class KeycloakServerConfig { private String host = "localhost"; private int port = 8081; + private String resourcesHome; public String getHost() { return host; @@ -73,6 +71,10 @@ public class KeycloakServer { return port; } + public String getResourcesHome() { + return resourcesHome; + } + public void setHost(String host) { this.host = host; } @@ -80,6 +82,10 @@ public class KeycloakServer { public void setPort(int port) { this.port = port; } + + public void setResourcesHome(String resourcesHome) { + this.resourcesHome = resourcesHome; + } } private static T loadJson(InputStream is, Class type) { @@ -109,6 +115,19 @@ public class KeycloakServer { } } + if (System.getProperties().containsKey("resources")) { + String resources = System.getProperty("resources"); + if (resources == null || resources.equals("")) { + for (String c : System.getProperty("java.class.path").split(File.pathSeparator)) { + if (c.contains("keycloak" + File.separator + "testsuite" + File.separator + "integration")) { + config.setResourcesHome(c.replaceFirst("testsuite.integration.*", "")); + } + } + } else { + config.setResourcesHome(resources); + } + } + final KeycloakServer keycloak = new KeycloakServer(config); keycloak.sysout = true; keycloak.start(); @@ -119,6 +138,10 @@ public class KeycloakServer { } } + if (System.getProperties().containsKey("import")) { + keycloak.importRealm(new FileInputStream(System.getProperty("import"))); + } + Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { @@ -193,22 +216,18 @@ public class KeycloakServer { } RealmModel defaultRealm = manager.createRealm(RealmModel.DEFAULT_REALM, RealmModel.DEFAULT_REALM); - manager.generateRealmKeys(defaultRealm); - + defaultRealm.setName(RealmModel.DEFAULT_REALM); defaultRealm.setEnabled(true); defaultRealm.setTokenLifespan(300); defaultRealm.setAccessCodeLifespan(60); defaultRealm.setAccessCodeLifespanUserAction(600); - defaultRealm.setSslNotRequired(false); + defaultRealm.setSslNotRequired(true); defaultRealm.setCookieLoginAllowed(true); defaultRealm.setRegistrationAllowed(true); - defaultRealm.setAutomaticRegistrationAfterSocialLogin(false); - defaultRealm.setVerifyEmail(false); - + manager.generateRealmKeys(defaultRealm); defaultRealm.addRequiredCredential(CredentialRepresentation.PASSWORD); - RoleModel role = defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE); - UserModel admin = defaultRealm.addUser("admin"); - defaultRealm.grantRole(admin, role); + defaultRealm.addRole(SaasService.REALM_CREATOR_ROLE); + defaultRealm.addDefaultRole(SaasService.REALM_CREATOR_ROLE); session.getTransaction().commit(); } finally { @@ -230,7 +249,7 @@ public class KeycloakServer { di.setClassLoader(getClass().getClassLoader()); di.setContextPath("/auth-server"); di.setDeploymentName("Keycloak"); - di.setResourceManager(new ClassPathResourceManager(FormService.class.getClassLoader(), "META-INF/resources")); + di.setResourceManager(new KeycloakResourceManager(config.getResourcesHome())); FilterInfo filter = Servlets.filter("SessionFilter", KeycloakSessionServletFilter.class); di.addFilter(filter); @@ -242,6 +261,10 @@ public class KeycloakServer { setupDefaultRealm(); + if (config.getResourcesHome() != null) { + info("Loading resources from " + config.getResourcesHome()); + } + info("Started Keycloak (http://" + config.getHost() + ":" + config.getPort() + "/auth-server) in " + (System.currentTimeMillis() - start) + " ms\n"); } @@ -261,4 +284,51 @@ public class KeycloakServer { info("Stopped Keycloak"); } + public static class KeycloakResourceManager implements ResourceManager { + + private String resourcesHome; + + public KeycloakResourceManager(String resourcesHome) { + this.resourcesHome = resourcesHome; + } + + @Override + public Resource getResource(String path) throws IOException { + if (resourcesHome == null) { + String realPath = "META-INF/resources" + path; + + if (realPath.endsWith("/admin/")) { + realPath += "index.html"; + } + + URL url = getClass().getClassLoader().getResource(realPath); + + return new URLResource(url, url.openConnection(), path); + } else { + File file; + if (path.startsWith("/forms")) { + file = file(resourcesHome, "forms", "src", "main", "resources", "META-INF", "resources", path.replace('/', File.separatorChar)); + } else if (path.startsWith("/admin")) { + file = file(resourcesHome, "admin-ui", "src", "main", "resources", "META-INF", "resources", path.replace('/', File.separatorChar)); + if (!file.isFile()) { + file = file(resourcesHome, "admin-ui-styles", "src", "main", "resources", "META-INF", "resources", path.replace('/', File.separatorChar)); + } + } else { + throw new IOException("Unknown resource " + path); + } + return new FileResource(file, new FileResourceManager(file.getParentFile(), 1), path); + } + } + } + + private static File file(String... path) { + StringBuilder s = new StringBuilder(); + s.append(path[0]); + for (int i = 1; i < path.length; i++) { + s.append(File.separator); + s.append(path[i]); + } + return new File(s.toString()); + } + } diff --git a/testsuite/src/main/java/org/keycloak/testutils/MailServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/MailServer.java similarity index 91% rename from testsuite/src/main/java/org/keycloak/testutils/MailServer.java rename to testsuite/integration/src/main/java/org/keycloak/testutils/MailServer.java index 42302e3575..8ba730a012 100644 --- a/testsuite/src/main/java/org/keycloak/testutils/MailServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/MailServer.java @@ -13,6 +13,9 @@ public class MailServer { GreenMail greenMail = new GreenMail(setup); greenMail.start(); + + System.out.println("Started mail server (localhost:3025)"); + System.out.println(); while (true) { int c = greenMail.getReceivedMessages().length; diff --git a/testsuite/src/main/java/org/keycloak/testutils/TotpGenerator.java b/testsuite/integration/src/main/java/org/keycloak/testutils/TotpGenerator.java similarity index 100% rename from testsuite/src/main/java/org/keycloak/testutils/TotpGenerator.java rename to testsuite/integration/src/main/java/org/keycloak/testutils/TotpGenerator.java diff --git a/testsuite/src/main/resources/META-INF/persistence.xml b/testsuite/integration/src/main/resources/META-INF/persistence.xml similarity index 100% rename from testsuite/src/main/resources/META-INF/persistence.xml rename to testsuite/integration/src/main/resources/META-INF/persistence.xml diff --git a/testsuite/src/test/java/org/keycloak/testsuite/ApplicationServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/ApplicationServlet.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/Constants.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/Constants.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/Constants.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/Constants.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/DummySocial.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/DummySocial.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocial.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/DummySocialServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/DummySocialServlet.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/DummySocialServlet.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/OAuthClient.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/forms/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/forms/AccountTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/forms/LoginTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountPasswordPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountTotpPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/AppPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/AppPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AppPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginConfigTotpPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordUpdatePage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/Page.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/Page.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/Page.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/pages/VerifyEmailPage.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/rule/WebResource.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebResource.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/rule/WebResource.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebResource.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/rule/WebRule.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java diff --git a/testsuite/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java similarity index 100% rename from testsuite/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java diff --git a/testsuite/src/test/resources/META-INF/services/org.keycloak.social.SocialProvider b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.social.SocialProvider similarity index 100% rename from testsuite/src/test/resources/META-INF/services/org.keycloak.social.SocialProvider rename to testsuite/integration/src/test/resources/META-INF/services/org.keycloak.social.SocialProvider diff --git a/testsuite/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json similarity index 100% rename from testsuite/src/test/resources/testrealm.json rename to testsuite/integration/src/test/resources/testrealm.json diff --git a/testsuite/performance/pom.xml b/testsuite/performance/pom.xml new file mode 100644 index 0000000000..1cb822099b --- /dev/null +++ b/testsuite/performance/pom.xml @@ -0,0 +1,240 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-1 + ../../pom.xml + + 4.0.0 + + keycloak-testsuite-performance + Keycloak Performance TestSuite + + + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-services + ${project.version} + + + org.jboss.resteasy + resteasy-jaxrs + provided + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + + + org.jboss.resteasy + jaxrs-api + provided + + + org.jboss.resteasy + resteasy-client + provided + + + org.apache.jmeter + ApacheJMeter_java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + + + performance-tests + + + + com.lazerycode.jmeter + jmeter-maven-plugin + + + jmeter-tests + verify + + jmeter + + + + + + org.keycloak + keycloak-testsuite + ${project.version} + test-jar + + + org.keycloak + keycloak-services + ${project.version} + + + org.jboss.resteasy + jaxrs-api + ${resteasy.version} + + + org.jboss.resteasy + resteasy-jaxrs + ${resteasy.version} + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + commons-io + commons-io + + + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + + + org.picketlink + picketlink-idm-impl + ${picketlink.version} + + + org.picketlink + picketlink-idm-simple-schema + ${picketlink.version} + + + org.picketlink + picketlink-config + ${picketlink.version} + + + org.mongodb + mongo-java-driver + ${mongo.driver.version} + + + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + ${hibernate.javax.persistence.version} + + + com.h2database + h2 + ${h2.version} + + + org.hibernate + hibernate-entitymanager + ${hibernate.entitymanager.version} + + + dom4j + dom4j + ${dom4j.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + mysql + mysql-connector-java + ${mysql.version} + + + + + + + com.lazerycode.jmeter + jmeter-analysis-maven-plugin + + + jmeter-tests-analyze + verify + + analyze + + + ${project.build.directory}/jmeter/results/*.jtl + ${project.build.directory}/jmeter/results + false + + + + + + + + + + + + + + + + + + + + \ 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 0000000000..efe57c128a --- /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 Marek Posolda + */ +public class BaseJMeterPerformanceTest extends AbstractJavaSamplerClient { + + + private static FutureTask factoryProvider = new FutureTask(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 0000000000..27499982b2 --- /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 Marek Posolda + */ +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 0000000000..29bd33cc92 --- /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 Marek Posolda + */ +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 ; iMarek Posolda + */ +public class PerfTestUtils { + + public static T readSystemProperty(String propertyName, Class 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 0000000000..416cd6029f --- /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 Marek Posolda + */ +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 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 0000000000..262ad932a6 --- /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 Marek Posolda + */ +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 0000000000..69732cf192 --- /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 Marek Posolda + */ +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 0000000000..39b6ce46ab --- /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 0000000000..b96dcf36d1 --- /dev/null +++ b/testsuite/performance/src/test/jmeter/keycloak_perf_test.jmx @@ -0,0 +1,39 @@ + + + + + + false + false + + + + + + + + continue + + false + 10 + + 1 + 0 + 1362689985000 + 1362689985000 + false + + + + + + + + + + org.keycloak.testsuite.performance.BaseJMeterPerformanceTest + + + + + \ 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 0000000000..26d24aab9b --- /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 0000000000..1dff64183f --- /dev/null +++ b/testsuite/performance/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,40 @@ + + + org.hibernate.ejb.HibernatePersistence + + org.picketlink.idm.jpa.model.sample.simple.AttributedTypeEntity + org.picketlink.idm.jpa.model.sample.simple.AccountTypeEntity + org.picketlink.idm.jpa.model.sample.simple.RoleTypeEntity + org.picketlink.idm.jpa.model.sample.simple.GroupTypeEntity + org.picketlink.idm.jpa.model.sample.simple.IdentityTypeEntity + org.picketlink.idm.jpa.model.sample.simple.RelationshipTypeEntity + org.picketlink.idm.jpa.model.sample.simple.RelationshipIdentityTypeEntity + org.picketlink.idm.jpa.model.sample.simple.PartitionTypeEntity + org.picketlink.idm.jpa.model.sample.simple.PasswordCredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.DigestCredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.X509CredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.OTPCredentialTypeEntity + org.picketlink.idm.jpa.model.sample.simple.AttributeTypeEntity + org.keycloak.services.models.picketlink.mappings.RealmEntity + org.keycloak.services.models.picketlink.mappings.ApplicationEntity + + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 7c5278d899..c8b7bcf19d 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -8,216 +8,13 @@ 4.0.0 - keycloak-testsuite - Keycloak TestSuite + keycloak-testsuite-pom + pom + Keycloak TestSuite + + integration + performance + - - - - org.keycloak - keycloak-as7-adapter - ${project.version} - - - - - - - org.bouncycastle - bcprov-jdk16 - - - org.keycloak - keycloak-core - ${project.version} - - - org.keycloak - keycloak-services - ${project.version} - - - - org.keycloak - keycloak-social-core - ${project.version} - - - org.keycloak - keycloak-social-google - ${project.version} - - - org.keycloak - keycloak-social-twitter - ${project.version} - - - org.keycloak - keycloak-social-facebook - ${project.version} - - - org.keycloak - keycloak-forms - ${project.version} - - - - org.jboss.logging - jboss-logging - - - org.picketlink - picketlink-idm-api - - - org.picketlink - picketlink-common - - - org.picketlink - picketlink-idm-impl - - - org.picketlink - picketlink-idm-simple-schema - - - org.picketlink - picketlink-config - - - org.jboss.resteasy - resteasy-jaxrs - - - log4j - log4j - - - org.slf4j - slf4j-api - - - org.slf4j - slf4j-simple - - - - - org.jboss.resteasy - jaxrs-api - - - org.jboss.resteasy - resteasy-client - - - org.jboss.resteasy - resteasy-crypto - - - org.jboss.resteasy - jose-jwt - - - org.jboss.resteasy - resteasy-undertow - - - io.undertow - undertow-servlet - - - io.undertow - undertow-core - - - org.codehaus.jackson - jackson-core-asl - - - org.codehaus.jackson - jackson-mapper-asl - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.0_spec - - - org.codehaus.jackson - jackson-xc - - - junit - junit - - - org.hibernate.javax.persistence - hibernate-jpa-2.0-api - - - com.h2database - h2 - 1.3.161 - - - org.hibernate - hibernate-entitymanager - 3.6.6.Final - - - com.icegreen - greenmail - - - org.seleniumhq.selenium - selenium-java - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.6 - 1.6 - - - - - - - - jboss-managed - - - org.jboss.as - jboss-as-arquillian-container-managed - test - 7.1.1.Final - - - - - jboss-remote - - - org.jboss.as - jboss-as-arquillian-container-remote - test - 7.1.1.Final - - - -