diff --git a/model/api/pom.xml b/model/api/pom.xml index 2c64294f4a..a5260d49d5 100755 --- a/model/api/pom.xml +++ b/model/api/pom.xml @@ -17,6 +17,17 @@ net.iharder base64 + + org.bouncycastle + bcprov-jdk16 + provided + + + org.keycloak + keycloak-core + ${project.version} + provided + junit junit diff --git a/model/api/src/main/java/org/keycloak/models/IdGenerator.java b/model/api/src/main/java/org/keycloak/models/IdGenerator.java deleted file mode 100755 index 3a1b028200..0000000000 --- a/model/api/src/main/java/org/keycloak/models/IdGenerator.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.keycloak.models; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class IdGenerator { - private static AtomicLong counter = new AtomicLong(1); - public static String generateId() { - return counter.getAndIncrement() + "-" + System.currentTimeMillis(); - } - -} diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java new file mode 100644 index 0000000000..4582a03274 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -0,0 +1,86 @@ +package org.keycloak.models.utils; + +import java.io.IOException; +import java.io.StringWriter; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import org.bouncycastle.openssl.PEMWriter; +import org.keycloak.models.RoleModel; +import org.keycloak.util.PemUtils; + +/** + * Set of helper methods, which are useful in various model implementations. + * + * @author Marek Posolda + */ +public final class KeycloakModelUtils { + + private KeycloakModelUtils() { + } + + private static AtomicLong counter = new AtomicLong(1); + + public static String generateId() { + return counter.getAndIncrement() + "-" + System.currentTimeMillis(); + } + + public static PublicKey getPublicKey(String publicKeyPem) { + if (publicKeyPem != null) { + try { + return PemUtils.decodePublicKey(publicKeyPem); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + return null; + } + } + + public static PrivateKey getPrivateKey(String privateKeyPem) { + if (privateKeyPem != null) { + try { + return PemUtils.decodePrivateKey(privateKeyPem); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + } + + public static String getPemFromKey(Key key) { + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try { + pemWriter.writeObject(key); + pemWriter.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + String s = writer.toString(); + return PemUtils.removeBeginEnd(s); + } + + /** + * Deep search if given role is descendant of composite role + * + * @param role role to check + * @param composite composite role + * @param visited set of already visited roles (used for recursion) + * @return true if "role" is descendant of "composite" + */ + public static boolean searchFor(RoleModel role, RoleModel composite, Set visited) { + if (visited.contains(composite)) return false; + visited.add(composite); + Set composites = composite.getComposites(); + if (composites.contains(role)) return true; + for (RoleModel contained : composites) { + if (!contained.isComposite()) continue; + if (searchFor(role, contained, visited)) return true; + } + return false; + } +} 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 deleted file mode 100644 index 57285db004..0000000000 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakSessionUtils.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.keycloak.models.utils; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * @author Marek Posolda - */ -public final class KeycloakSessionUtils { - - private KeycloakSessionUtils() { - } - - private static AtomicLong counter = new AtomicLong(1); - - public static String generateId() { - return counter.getAndIncrement() + "-" + System.currentTimeMillis(); - } -} diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelProviderUtils.java b/model/api/src/main/java/org/keycloak/models/utils/ModelProviderUtils.java new file mode 100644 index 0000000000..e4f64cca8e --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelProviderUtils.java @@ -0,0 +1,50 @@ +package org.keycloak.models.utils; + +import java.util.ServiceLoader; + +import org.keycloak.models.ModelProvider; + +/** + * @author Marek Posolda + */ +public class ModelProviderUtils { + + public static final String MODEL_PROVIDER = "keycloak.model"; + public static final String DEFAULT_MODEL_PROVIDER = "jpa"; + + public static Iterable getRegisteredProviders() { + return ServiceLoader.load(ModelProvider.class); + } + + public static ModelProvider getConfiguredModelProvider(Iterable providers) { + String configuredProvider = System.getProperty(MODEL_PROVIDER); + ModelProvider provider = null; + + if (configuredProvider != null) { + for (ModelProvider p : providers) { + if (p.getId().equals(configuredProvider)) { + provider = p; + } + } + } else { + for (ModelProvider p : providers) { + if (provider == null) { + provider = p; + } + + if (p.getId().equals(DEFAULT_MODEL_PROVIDER)) { + provider = p; + break; + } + } + } + + return provider; + } + + public static ModelProvider getConfiguredModelProvider() { + return getConfiguredModelProvider(getRegisteredProviders()); + } + + +} diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml index 7ba6691273..ecbf28940f 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -59,6 +59,20 @@ + + + org.keycloak + keycloak-model-tests + ${project.version} + tests + test + + + com.h2database + h2 + test + + @@ -70,6 +84,22 @@ 1.6 + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + org.keycloak:keycloak-model-tests + + + + + + diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java index 7b7db28237..a072c75cc6 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java @@ -2,7 +2,7 @@ package org.keycloak.models.jpa; import org.keycloak.models.*; import org.keycloak.models.jpa.entities.*; -import org.keycloak.models.utils.KeycloakSessionUtils; +import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; @@ -28,7 +28,7 @@ public class JpaKeycloakSession implements KeycloakSession { @Override public RealmModel createRealm(String name) { - return createRealm(KeycloakSessionUtils.generateId(), name); + return createRealm(KeycloakModelUtils.generateId(), name); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 965cbbee4f..467953ca54 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -13,6 +13,7 @@ import org.keycloak.models.jpa.entities.SocialLinkEntity; import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.jpa.entities.UserRoleMappingEntity; import org.keycloak.models.jpa.entities.UserScopeMappingEntity; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.Pbkdf2PasswordEncoder; import org.keycloak.util.PemUtils; import org.keycloak.models.ApplicationModel; @@ -187,59 +188,29 @@ public class RealmAdapter implements RealmModel { @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); - } - } + publicKey = KeycloakModelUtils.getPublicKey(getPublicKeyPem()); 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)); + String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); + setPublicKeyPem(publicKeyPem); } @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); - } - } + privateKey = KeycloakModelUtils.getPrivateKey(getPrivateKeyPem()); 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)); + String privateKeyPem = KeycloakModelUtils.getPemFromKey(privateKey); + setPrivateKeyPem(privateKeyPem); } protected RequiredCredentialModel initRequiredCredentialModel(String type) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java index 2c9a8c95a9..e418441de2 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java @@ -6,6 +6,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.jpa.entities.ApplicationRoleEntity; import org.keycloak.models.jpa.entities.RealmRoleEntity; import org.keycloak.models.jpa.entities.RoleEntity; +import org.keycloak.models.utils.KeycloakModelUtils; import javax.persistence.EntityManager; import java.util.HashSet; @@ -94,27 +95,13 @@ public class RoleAdapter implements RoleModel { return set; } - public static boolean searchFor(RoleModel role, RoleModel composite, Set visited) { - if (visited.contains(composite)) return false; - visited.add(composite); - Set composites = composite.getComposites(); - if (composites.contains(role)) return true; - for (RoleModel contained : composites) { - if (!contained.isComposite()) continue; - if (searchFor(role, contained, visited)) return true; - } - return false; - } - - - @Override public boolean hasRole(RoleModel role) { if (this.equals(role)) return true; if (!isComposite()) return false; Set visited = new HashSet(); - return searchFor(role, this, visited); + return KeycloakModelUtils.searchFor(role, this, visited); } @Override diff --git a/services/src/test/resources/META-INF/persistence.xml b/model/jpa/src/test/resources/META-INF/persistence.xml similarity index 100% rename from services/src/test/resources/META-INF/persistence.xml rename to model/jpa/src/test/resources/META-INF/persistence.xml diff --git a/model/mongo/pom.xml b/model/mongo/pom.xml index 537112a618..fadcc7f268 100755 --- a/model/mongo/pom.xml +++ b/model/mongo/pom.xml @@ -5,7 +5,7 @@ keycloak-parent org.keycloak - 1.0-alpha-1 + 1.0-alpha-2-SNAPSHOT ../../pom.xml 4.0.0 @@ -41,27 +41,28 @@ 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 + org.keycloak + keycloak-model-tests + ${project.version} + tests test + + + localhost + 27018 + keycloak + true + + @@ -72,6 +73,66 @@ 1.6 + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test + integration-test + + test + + + + ${keycloak.mongo.host} + ${keycloak.mongo.port} + ${keycloak.mongo.db} + ${keycloak.mongo.clearOnStartup} + + + org.keycloak:keycloak-model-tests + + + + + default-test + + true + + + + + + + + com.github.joelittlejohn.embedmongo + embedmongo-maven-plugin + + + start-mongodb + pre-integration-test + + start + + + ${keycloak.mongo.port} + file + ${project.build.directory}/mongodb.log + + + + stop-mongodb + post-integration-test + + stop + + + + + \ No newline at end of file diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/MongoDBSessionFactoryTestContext.java b/model/mongo/src/main/java/org/keycloak/models/mongo/MongoDBSessionFactoryTestContext.java deleted file mode 100755 index 1b00fa5b54..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/MongoDBSessionFactoryTestContext.java +++ /dev/null @@ -1,57 +0,0 @@ -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.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/model/mongo/src/main/java/org/keycloak/models/mongo/MongoRunnerListener.java b/model/mongo/src/main/java/org/keycloak/models/mongo/MongoRunnerListener.java deleted file mode 100755 index a1f67883c8..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/MongoRunnerListener.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.keycloak.services.listeners; - -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; - -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - -/** - * @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/model/mongo/src/main/java/org/keycloak/models/mongo/PropertiesManager.java b/model/mongo/src/main/java/org/keycloak/models/mongo/PropertiesManager.java deleted file mode 100755 index 9e897ae238..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/PropertiesManager.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.keycloak.models.mongo; - -/** - * @author Marek Posolda - */ -public class PropertiesManager { - - 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 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/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 deleted file mode 100644 index 81546ba469..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractAttributedNoSQLObject.java +++ /dev/null @@ -1,37 +0,0 @@ -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/AbstractMongoIdentifiableEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractMongoIdentifiableEntity.java new file mode 100644 index 0000000000..86e6e2e31f --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractMongoIdentifiableEntity.java @@ -0,0 +1,50 @@ +package org.keycloak.models.mongo.api; + +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +/** + * @author Marek Posolda + */ +public class AbstractMongoIdentifiableEntity implements MongoIdentifiableEntity { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public void afterRemove(MongoStoreInvocationContext invocationContext) { + // Empty by default + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + + if (this.id == null) return false; + + if (o == null || getClass() != o.getClass()) return false; + + AbstractMongoIdentifiableEntity that = (AbstractMongoIdentifiableEntity) o; + + if (!getId().equals(that.getId())) return false; + + return true; + + } + + @Override + public int hashCode() { + return id!=null ? id.hashCode() : super.hashCode(); + } + + @Override + public String toString() { + return String.format("%s [ id=%s ]", getClass().getSimpleName(), getId()); + } +} 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 deleted file mode 100644 index 837e5e4644..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/AbstractNoSQLObject.java +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 45accd1c1c..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/AttributedNoSQLObject.java +++ /dev/null @@ -1,17 +0,0 @@ -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/NoSQLCollection.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoCollection.java similarity index 92% rename from model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLCollection.java rename to model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoCollection.java index 80b63326f0..8695d12393 100644 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLCollection.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoCollection.java @@ -15,7 +15,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Documented @Retention(RUNTIME) @Inherited -public @interface NoSQLCollection { +public @interface MongoCollection { String collectionName(); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoEntity.java new file mode 100644 index 0000000000..8b91583344 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoEntity.java @@ -0,0 +1,11 @@ +package org.keycloak.models.mongo.api; + +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +/** + * Base interface for object, which is persisted in Mongo + * + * @author Marek Posolda + */ +public interface MongoEntity { +} 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/MongoField.java similarity index 94% rename from model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLField.java rename to model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoField.java index 3af69a7135..4f19f83e7d 100644 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLField.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoField.java @@ -14,7 +14,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({METHOD, FIELD}) @Documented @Retention(RUNTIME) -public @interface NoSQLField { +public @interface MongoField { // TODO: fieldName add lazy loading? } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoIdentifiableEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoIdentifiableEntity.java new file mode 100644 index 0000000000..dfa553ee13 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoIdentifiableEntity.java @@ -0,0 +1,21 @@ +package org.keycloak.models.mongo.api; + +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +/** + * Entity with Id + * + * @author Marek Posolda + */ +public interface MongoIdentifiableEntity extends MongoEntity { + + public String getId(); + + public void setId(String id); + + /** + * Lifecycle callback, which is called after removal of this object from Mongo. + * It may be useful for triggering removal of wired objects. + */ + void afterRemove(MongoStoreInvocationContext invocationContext); +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoStore.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoStore.java new file mode 100755 index 0000000000..9da25e5b4e --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/MongoStore.java @@ -0,0 +1,43 @@ +package org.keycloak.models.mongo.api; + +import com.mongodb.DBObject; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +import java.util.List; + +/** + * @author Marek Posolda + */ +public interface MongoStore { + + /** + * Insert new entity + * + * @param entity to insert + */ + void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context); + + /** + * Update existing entity + * + * @param entity to update + */ + void updateEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context); + + + T loadEntity(Class type, String id, MongoStoreInvocationContext context); + + T loadSingleEntity(Class type, DBObject query, MongoStoreInvocationContext context); + + List loadEntities(Class type, DBObject query, MongoStoreInvocationContext context); + + boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context); + + boolean removeEntity(Class type, String id, MongoStoreInvocationContext context); + + boolean removeEntities(Class type, DBObject query, MongoStoreInvocationContext context); + + boolean pushItemToList(MongoIdentifiableEntity entity, String listPropertyName, S itemToPush, boolean skipIfAlreadyPresent, MongoStoreInvocationContext context); + + boolean pullItemFromList(MongoIdentifiableEntity entity, String listPropertyName, S itemToPull, MongoStoreInvocationContext context); +} 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 deleted file mode 100755 index 0a63606e2d..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQL.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.keycloak.models.mongo.api; - -import org.keycloak.models.mongo.api.query.NoSQLQuery; -import org.keycloak.models.mongo.api.query.NoSQLQueryBuilder; - -import java.util.List; - -/** - * @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/NoSQLId.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLId.java deleted file mode 100644 index 06ed01e655..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLId.java +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 0242243936..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/NoSQLObject.java +++ /dev/null @@ -1,16 +0,0 @@ -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/context/MongoStoreInvocationContext.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/context/MongoStoreInvocationContext.java new file mode 100644 index 0000000000..358f445d09 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/context/MongoStoreInvocationContext.java @@ -0,0 +1,32 @@ +package org.keycloak.models.mongo.api.context; + +import org.keycloak.models.mongo.api.MongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoStore; + +/** + * Context, which provides callback methods to be invoked by MongoStore + * + * @author Marek Posolda + */ +public interface MongoStoreInvocationContext { + + void addCreatedObject(MongoIdentifiableEntity entity); + + void addLoadedObject(MongoIdentifiableEntity entity); + + T getLoadedObject(Class type, String id); + + void addUpdateTask(MongoIdentifiableEntity entityToUpdate, MongoTask task); + + void addRemovedObject(MongoIdentifiableEntity entityToRemove); + + void beforeDBSearch(Class entityType); + + void begin(); + + void commit(); + + void rollback(); + + MongoStore getMongoStore(); +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/context/MongoTask.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/context/MongoTask.java new file mode 100644 index 0000000000..0c1d500b69 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/context/MongoTask.java @@ -0,0 +1,13 @@ +package org.keycloak.models.mongo.api.context; + +import org.keycloak.models.mongo.api.MongoStore; + +/** + * @author Marek Posolda + */ +public interface MongoTask { + + void execute(); + + boolean isFullUpdate(); +} 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 deleted file mode 100644 index 29cc0f31ab..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQuery.java +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index dcdb5752a3..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/query/NoSQLQueryBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index a6b6c869e6..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/Converter.java +++ /dev/null @@ -1,16 +0,0 @@ -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/Mapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/Mapper.java new file mode 100644 index 0000000000..c93acde086 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/Mapper.java @@ -0,0 +1,22 @@ +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 mappers should be registered in TypeMapper, which is main entry point to be used by application + * + * @author Marek Posolda + */ +public interface Mapper { + + /** + * Convert object from one type to expected type + * + * @param mapperContext Encapsulates reference to converted object and other things, which might be helpful in conversion + * @return converted object + */ + S convertObject(MapperContext mapperContext); + + Class getTypeOfObjectToConvert(); + + Class getExpectedReturnType(); +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/MapperContext.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/MapperContext.java new file mode 100644 index 0000000000..987f18b0a4 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/MapperContext.java @@ -0,0 +1,36 @@ +package org.keycloak.models.mongo.api.types; + +import java.util.List; + +/** + * @author Marek Posolda + */ +public class MapperContext { + + // object to convert + private final T objectToConvert; + + // expected return type, which could be useful information in some converters, so they are able to dynamically instantiate types + private final Class expectedReturnType; + + // in case that expected return type is generic type (like "List"), then genericTypes could contain list of expected generic arguments + private final List> genericTypes; + + public MapperContext(T objectToConvert, Class expectedReturnType, List> genericTypes) { + this.objectToConvert = objectToConvert; + this.expectedReturnType = expectedReturnType; + this.genericTypes = genericTypes; + } + + public T getObjectToConvert() { + return objectToConvert; + } + + public Class getExpectedReturnType() { + return expectedReturnType; + } + + public List> getGenericTypes() { + return genericTypes; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/MapperRegistry.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/MapperRegistry.java new file mode 100755 index 0000000000..2c1c270c9f --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/MapperRegistry.java @@ -0,0 +1,111 @@ +package org.keycloak.models.mongo.api.types; + +import java.util.HashMap; +import java.util.Map; + +/** + * Registry of mappers, which allow to convert application object to database objects. MapperRegistry is main entry point to be used by application. + * Application can create instance of MapperRegistry and then register required Mapper objects. + * + * @author Marek Posolda + */ +public class MapperRegistry { + + // TODO: Thread-safety support (maybe...) + // Mappers of Application objects to DB objects + private Map, Mapper> appObjectMappers = new HashMap, Mapper>(); + + // Mappers of DB objects to Application objects + private Map, Map, Mapper>> dbObjectMappers = new HashMap, Map, Mapper>>(); + + + /** + * Add mapper for converting application objects to DB objects + * + * @param mapper + */ + public void addAppObjectMapper(Mapper mapper) { + appObjectMappers.put(mapper.getTypeOfObjectToConvert(), mapper); + } + + + /** + * Add mapper for converting DB objects to application objects + * + * @param mapper + */ + public void addDBObjectMapper(Mapper mapper) { + Class dbObjectType = mapper.getTypeOfObjectToConvert(); + Class appObjectType = mapper.getExpectedReturnType(); + Map, Mapper> appObjects = dbObjectMappers.get(dbObjectType); + if (appObjects == null) { + appObjects = new HashMap, Mapper>(); + dbObjectMappers.put(dbObjectType, appObjects); + } + appObjects.put(appObjectType, mapper); + } + + + public S convertDBObjectToApplicationObject(MapperContext context) { + Object dbObject = context.getObjectToConvert(); + Class expectedApplicationObjectType = context.getExpectedReturnType(); + + Class dbObjectType = dbObject.getClass(); + Mapper mapper; + + Map, Mapper> appObjects = dbObjectMappers.get(dbObjectType); + if (appObjects == null) { + throw new IllegalArgumentException("Not found any mappers for type " + dbObjectType); + } else { + if (appObjects.size() == 1) { + mapper = (Mapper)appObjects.values().iterator().next(); + } else { + // Try to find converter for requested application type + mapper = (Mapper)getAppConverterForType(context.getExpectedReturnType(), appObjects); + } + } + + if (mapper == null) { + throw new IllegalArgumentException("Can't found mapper for type " + dbObjectType + " and expectedApplicationType " + expectedApplicationObjectType); + } + + return mapper.convertObject(context); + } + + + public S convertApplicationObjectToDBObject(Object applicationObject, Class expectedDBObjectType) { + Class appObjectType = applicationObject.getClass(); + Mapper mapper = (Mapper)getAppConverterForType(appObjectType, appObjectMappers); + if (mapper == null) { + throw new IllegalArgumentException("Can't found converter for type " + appObjectType + " in registered appObjectMappers"); + } + if (!expectedDBObjectType.isAssignableFrom(mapper.getExpectedReturnType())) { + throw new IllegalArgumentException("Converter " + mapper + " has return type " + mapper.getExpectedReturnType() + + " but we need type " + expectedDBObjectType); + } + return mapper.convertObject(new MapperContext(applicationObject, expectedDBObjectType, null)); + } + + // Try to find converter for given type or all it's supertypes + private static Mapper getAppConverterForType(Class appObjectType, Map, Mapper> appObjectConverters) { + Mapper mapper = (Mapper)appObjectConverters.get(appObjectType); + if (mapper != null) { + return mapper; + } else { + Class[] interfaces = appObjectType.getInterfaces(); + for (Class interface1 : interfaces) { + mapper = getAppConverterForType(interface1, appObjectConverters); + if (mapper != null) { + return mapper; + } + } + + 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/api/types/TypeConverter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/TypeConverter.java deleted file mode 100755 index e097930774..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/api/types/TypeConverter.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.keycloak.models.mongo.api.types; - -import java.util.HashMap; -import java.util.Map; - -/** - * 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/ObjectInfo.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/EntityInfo.java similarity index 67% rename from model/mongo/src/main/java/org/keycloak/models/mongo/impl/ObjectInfo.java rename to model/mongo/src/main/java/org/keycloak/models/mongo/impl/EntityInfo.java index b511626d9d..66eda952b7 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/ObjectInfo.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/EntityInfo.java @@ -1,6 +1,6 @@ package org.keycloak.models.mongo.impl; -import org.keycloak.models.mongo.api.NoSQLObject; +import org.keycloak.models.mongo.api.MongoEntity; import org.picketlink.common.properties.Property; import java.util.Collection; @@ -12,20 +12,17 @@ import java.util.Map; /** * @author Marek Posolda */ -public class ObjectInfo { +public class EntityInfo { - private final Class objectClass; + 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) { + public EntityInfo(Class objectClass, String dbCollectionName, List> properties) { this.objectClass = objectClass; this.dbCollectionName = dbCollectionName; - this.oidProperty = oidProperty; Map> props= new HashMap>(); for (Property property : properties) { @@ -34,7 +31,7 @@ public class ObjectInfo { this.properties = Collections.unmodifiableMap(props); } - public Class getObjectClass() { + public Class getObjectClass() { return objectClass; } @@ -42,10 +39,6 @@ public class ObjectInfo { return dbCollectionName; } - public Property getOidProperty() { - return oidProperty; - } - public Collection> getProperties() { return properties.values(); } 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 deleted file mode 100755 index 6bacedb288..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBImpl.java +++ /dev/null @@ -1,324 +0,0 @@ -package org.keycloak.models.mongo.impl; - -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.BasicDBListConverter; -import org.keycloak.models.mongo.impl.types.BasicDBObjectConverter; -import org.keycloak.models.mongo.impl.types.EnumToStringConverter; -import org.keycloak.models.mongo.impl.types.ListConverter; -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; - -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; - -/** - * @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 deleted file mode 100755 index 2d1f61de6a..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoDBQueryBuilder.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.keycloak.models.mongo.impl; - -import com.mongodb.BasicDBObject; -import org.bson.types.ObjectId; -import org.keycloak.models.mongo.api.query.NoSQLQueryBuilder; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -/** - * @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/MongoStoreImpl.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoStoreImpl.java new file mode 100755 index 0000000000..0bbc094c7b --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/MongoStoreImpl.java @@ -0,0 +1,420 @@ +package org.keycloak.models.mongo.impl; + +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.MongoCollection; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; +import org.keycloak.models.mongo.api.MongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoStore; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.api.context.MongoTask; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; +import org.keycloak.models.mongo.api.types.MapperRegistry; +import org.keycloak.models.mongo.impl.types.BasicDBListMapper; +import org.keycloak.models.mongo.impl.types.BasicDBObjectMapper; +import org.keycloak.models.mongo.impl.types.BasicDBObjectToMapMapper; +import org.keycloak.models.mongo.impl.types.EnumToStringMapper; +import org.keycloak.models.mongo.impl.types.ListMapper; +import org.keycloak.models.mongo.impl.types.MapMapper; +import org.keycloak.models.mongo.impl.types.MongoEntityMapper; +import org.keycloak.models.mongo.impl.types.SimpleMapper; +import org.keycloak.models.mongo.impl.types.StringToEnumMapper; +import org.picketlink.common.properties.Property; +import org.picketlink.common.properties.query.AnnotatedPropertyCriteria; +import org.picketlink.common.properties.query.PropertyQueries; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * @author Marek Posolda + */ +public class MongoStoreImpl implements MongoStore { + + private static final Class[] SIMPLE_TYPES = { String.class, Integer.class, Boolean.class, Long.class, Double.class, Character.class, Date.class, byte[].class }; + + private final DB database; + private static final Logger logger = Logger.getLogger(MongoStoreImpl.class); + + private final MapperRegistry mapperRegistry; + private ConcurrentMap, EntityInfo> entityInfoCache = + new ConcurrentHashMap, EntityInfo>(); + + + public MongoStoreImpl(DB database, boolean clearCollectionsOnStartup, Class[] managedEntityTypes) { + this.database = database; + + mapperRegistry = new MapperRegistry(); + + for (Class simpleConverterClass : SIMPLE_TYPES) { + SimpleMapper converter = new SimpleMapper(simpleConverterClass); + mapperRegistry.addAppObjectMapper(converter); + mapperRegistry.addDBObjectMapper(converter); + } + + // Specific converter for ArrayList is added just for performance purposes to avoid recursive converter lookup (most of list impl will be ArrayList) + mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, ArrayList.class)); + mapperRegistry.addAppObjectMapper(new ListMapper(mapperRegistry, List.class)); + mapperRegistry.addDBObjectMapper(new BasicDBListMapper(mapperRegistry)); + + mapperRegistry.addAppObjectMapper(new MapMapper(HashMap.class)); + mapperRegistry.addAppObjectMapper(new MapMapper(Map.class)); + mapperRegistry.addDBObjectMapper(new BasicDBObjectToMapMapper()); + + // Enum converters + mapperRegistry.addAppObjectMapper(new EnumToStringMapper()); + mapperRegistry.addDBObjectMapper(new StringToEnumMapper()); + + for (Class type : managedEntityTypes) { + getEntityInfo(type); + mapperRegistry.addAppObjectMapper(new MongoEntityMapper(this, mapperRegistry, type)); + mapperRegistry.addDBObjectMapper(new BasicDBObjectMapper(this, mapperRegistry, type)); + } + + if (clearCollectionsOnStartup) { + // dropDatabase(); + clearManagedCollections(managedEntityTypes); + } + } + + protected void dropDatabase() { + this.database.dropDatabase(); + logger.info("Database " + this.database.getName() + " dropped in MongoDB"); + } + + // Don't drop database, but just clear all data in managed collections (useful for development) + protected void clearManagedCollections(Class[] managedEntityTypes) { + for (Class clazz : managedEntityTypes) { + DBCollection dbCollection = getDBCollectionForType(clazz); + if (dbCollection != null) { + dbCollection.remove(new BasicDBObject()); + logger.debug("Collection " + dbCollection.getName() + " cleared from " + this.database.getName()); + } + } + } + + @Override + public void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) { + Class clazz = entity.getClass(); + + // Find annotations for ID, for all the properties and for the name of the collection. + EntityInfo entityInfo = getEntityInfo(clazz); + + // Create instance of BasicDBObject and add all declared properties to it (properties with null value probably should be skipped) + BasicDBObject dbObject = mapperRegistry.convertApplicationObjectToDBObject(entity, BasicDBObject.class); + + DBCollection dbCollection = database.getCollection(entityInfo.getDbCollectionName()); + + String currentId = entity.getId(); + + // Inserting object, which already has oid property set. So we need to set "_id" + if (currentId != null) { + dbObject.put("_id", getObjectId(currentId)); + } + + dbCollection.insert(dbObject); + + // Add id to value of given object + if (currentId == null) { + entity.setId(dbObject.getString("_id")); + } + + // Treat object as if it is read (It is already submited to transaction) + context.addLoadedObject(entity); + } + + @Override + public void updateEntity(final MongoIdentifiableEntity entity, MongoStoreInvocationContext context) { + MongoTask fullUpdateTask = new MongoTask() { + + @Override + public void execute() { + Class clazz = entity.getClass(); + EntityInfo entityInfo = getEntityInfo(clazz); + BasicDBObject dbObject = mapperRegistry.convertApplicationObjectToDBObject(entity, BasicDBObject.class); + DBCollection dbCollection = database.getCollection(entityInfo.getDbCollectionName()); + + String currentId = entity.getId(); + + if (currentId == null) { + throw new IllegalStateException("Can't update entity without id: " + entity); + } else { + BasicDBObject query = new BasicDBObject("_id", getObjectId(currentId)); + dbCollection.update(query, dbObject); + } + } + + @Override + public boolean isFullUpdate() { + return true; + } + }; + + // update is just added to context and postponed + context.addUpdateTask(entity, fullUpdateTask); + } + + + @Override + public T loadEntity(Class type, String id, MongoStoreInvocationContext context) { + // First look if we already read the object with this oid and type during this transaction. If yes, use it instead of DB lookup + T cached = context.getLoadedObject(type, id); + if (cached != null) return cached; + + DBCollection dbCollection = getDBCollectionForType(type); + + BasicDBObject idQuery = new BasicDBObject("_id", getObjectId(id)); + DBObject dbObject = dbCollection.findOne(idQuery); + + if (dbObject == null) return null; + + MapperContext mapperContext = new MapperContext(dbObject, type, null); + T converted = mapperRegistry.convertDBObjectToApplicationObject(mapperContext); + + // Now add it to loaded objects + context.addLoadedObject(converted); + + return converted; + } + + + @Override + public T loadSingleEntity(Class type, DBObject query, MongoStoreInvocationContext context) { + List result = loadEntities(type, query, context); + 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 loadEntities(Class type, DBObject query, MongoStoreInvocationContext context) { + // First we should execute all pending tasks before searching DB + context.beforeDBSearch(type); + + DBCollection dbCollection = getDBCollectionForType(type); + DBCursor cursor = dbCollection.find(query); + + return convertCursor(type, cursor, context); + } + + + @Override + public boolean removeEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) { + return removeEntity(entity.getClass(), entity.getId(), context); + } + + + @Override + public boolean removeEntity(Class type, String id, MongoStoreInvocationContext context) { + MongoIdentifiableEntity found = loadEntity(type, id, context); + if (found == null) { + return false; + } else { + DBCollection dbCollection = getDBCollectionForType(type); + BasicDBObject dbQuery = new BasicDBObject("_id", getObjectId(id)); + dbCollection.remove(dbQuery); + logger.info("Entity of type: " + type + ", id: " + id + " removed from MongoDB."); + + context.addRemovedObject(found); + return true; + } + } + + + @Override + public boolean removeEntities(Class type, DBObject query, MongoStoreInvocationContext context) { + List foundObjects = loadEntities(type, query, context); + if (foundObjects.size() == 0) { + return false; + } else { + DBCollection dbCollection = getDBCollectionForType(type); + dbCollection.remove(query); + logger.info("Removed " + foundObjects.size() + " entities of type: " + type + ", query: " + query); + + for (MongoIdentifiableEntity found : foundObjects) { + context.addRemovedObject(found);; + } + return true; + } + } + + @Override + public boolean pushItemToList(final MongoIdentifiableEntity entity, final String listPropertyName, S itemToPush, boolean skipIfAlreadyPresent, MongoStoreInvocationContext context) { + final Class type = entity.getClass(); + EntityInfo entityInfo = getEntityInfo(type); + + // Add item to list directly in this object + Property listProperty = entityInfo.getPropertyByName(listPropertyName); + if (listProperty == null) { + throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + entity); + } + + List list = (List)listProperty.getValue(entity); + if (list == null) { + list = new ArrayList(); + listProperty.setValue(entity, list); + } + + // Skip if item is already in list + if (skipIfAlreadyPresent && list.contains(itemToPush)) { + return false; + } + + // Update java object + list.add(itemToPush); + + // Add update of list to pending tasks + final List listt = list; + context.addUpdateTask(entity, new MongoTask() { + + @Override + public void execute() { + // Now DB update of new list with usage of $set + BasicDBList dbList = mapperRegistry.convertApplicationObjectToDBObject(listt, BasicDBList.class); + + BasicDBObject query = new BasicDBObject("_id", getObjectId(entity.getId())); + BasicDBObject listObject = new BasicDBObject(listPropertyName, dbList); + BasicDBObject setCommand = new BasicDBObject("$set", listObject); + getDBCollectionForType(type).update(query, setCommand); + } + + @Override + public boolean isFullUpdate() { + return false; + } + }); + + return true; + } + + + @Override + public boolean pullItemFromList(final MongoIdentifiableEntity entity, final String listPropertyName, final S itemToPull, MongoStoreInvocationContext context) { + final Class type = entity.getClass(); + EntityInfo entityInfo = getEntityInfo(type); + + // Remove item from list directly in this object + Property listProperty = entityInfo.getPropertyByName(listPropertyName); + if (listProperty == null) { + throw new IllegalArgumentException("Property " + listPropertyName + " doesn't exist on object " + entity); + } + List list = (List)listProperty.getValue(entity); + + // If list is null, we skip both object and DB update + if (list == null || !list.contains(itemToPull)) { + return false; + } else { + + // Update java object + list.remove(itemToPull); + + // Add update of list to pending tasks + context.addUpdateTask(entity, new MongoTask() { + + @Override + public void execute() { + // Pull item from DB + Object dbItemToPull = mapperRegistry.convertApplicationObjectToDBObject(itemToPull, Object.class); + BasicDBObject query = new BasicDBObject("_id", getObjectId(entity.getId())); + BasicDBObject pullObject = new BasicDBObject(listPropertyName, dbItemToPull); + BasicDBObject pullCommand = new BasicDBObject("$pull", pullObject); + getDBCollectionForType(type).update(query, pullCommand); + } + + @Override + public boolean isFullUpdate() { + return false; + } + }); + + return true; + } + } + + // Possibility to add user-defined converters + public void addAppObjectConverter(Mapper mapper) { + mapperRegistry.addAppObjectMapper(mapper); + } + + public void addDBObjectConverter(Mapper mapper) { + mapperRegistry.addDBObjectMapper(mapper); + } + + public EntityInfo getEntityInfo(Class objectClass) { + EntityInfo entityInfo = entityInfoCache.get(objectClass); + if (entityInfo == null) { + List> properties = PropertyQueries.createQuery(objectClass).addCriteria(new AnnotatedPropertyCriteria(MongoField.class)).getResultList(); + + MongoCollection classAnnotation = objectClass.getAnnotation(MongoCollection.class); + + String dbCollectionName = classAnnotation==null ? null : classAnnotation.collectionName(); + entityInfo = new EntityInfo(objectClass, dbCollectionName, properties); + + EntityInfo existing = entityInfoCache.putIfAbsent(objectClass, entityInfo); + if (existing != null) { + entityInfo = existing; + } + } + + return entityInfo; + } + + protected List convertCursor(Class type, DBCursor cursor, MongoStoreInvocationContext context) { + List result = new ArrayList(); + + try { + for (DBObject dbObject : cursor) { + // First look if we already have loaded object cached. If yes, we will use cached instance + String id = dbObject.get("_id").toString(); + T object = context.getLoadedObject(type, id); + + if (object == null) { + // So convert and use fresh instance from DB + MapperContext mapperContext = new MapperContext(dbObject, type, null); + object = mapperRegistry.convertDBObjectToApplicationObject(mapperContext); + context.addLoadedObject(object); + } + + result.add(object); + } + } finally { + cursor.close(); + } + + return result; + } + + protected DBCollection getDBCollectionForType(Class type) { + EntityInfo entityInfo = getEntityInfo(type); + String dbCollectionName = entityInfo.getDbCollectionName(); + return dbCollectionName==null ? null : database.getCollection(entityInfo.getDbCollectionName()); + } + + // We allow ObjectId to be both "ObjectId" or "String". + protected Object getObjectId(String idAsString) { + if (ObjectId.isValid(idAsString)) { + return new ObjectId(idAsString); + } else { + return idAsString; + } + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/context/SimpleMongoStoreInvocationContext.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/context/SimpleMongoStoreInvocationContext.java new file mode 100644 index 0000000000..b0c871d83e --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/context/SimpleMongoStoreInvocationContext.java @@ -0,0 +1,65 @@ +package org.keycloak.models.mongo.impl.context; + +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoStore; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.api.context.MongoTask; + +/** + * Context, which is not doing any postponing of tasks and does not cache anything + * + * @author Marek Posolda + */ +public class SimpleMongoStoreInvocationContext implements MongoStoreInvocationContext { + + private final MongoStore mongoStore; + + public SimpleMongoStoreInvocationContext(MongoStore mongoStore) { + this.mongoStore = mongoStore; + } + + @Override + public void addCreatedObject(MongoIdentifiableEntity entity) { + } + + @Override + public void addLoadedObject(MongoIdentifiableEntity entity) { + } + + @Override + public T getLoadedObject(Class type, String id) { + return null; + } + + @Override + public void addUpdateTask(MongoIdentifiableEntity entityToUpdate, MongoTask task) { + task.execute(); + } + + @Override + public void addRemovedObject(MongoIdentifiableEntity entityToRemove) { + entityToRemove.afterRemove(this); + } + + @Override + public void beforeDBSearch(Class entityType) { + } + + @Override + public void begin() { + } + + @Override + public void commit() { + } + + @Override + public void rollback() { + } + + @Override + public MongoStore getMongoStore() { + return mongoStore; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/context/TransactionMongoStoreInvocationContext.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/context/TransactionMongoStoreInvocationContext.java new file mode 100644 index 0000000000..5d5ff08276 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/context/TransactionMongoStoreInvocationContext.java @@ -0,0 +1,140 @@ +package org.keycloak.models.mongo.impl.context; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.keycloak.models.mongo.api.MongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoStore; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.api.context.MongoTask; + +/** + * Invocation context, which has some very basic support for transactions, and is able to cache loaded objects. + * It always execute all pending update tasks before start searching for other objects + * + * It's per-request object (not thread safe) + * + * @author Marek Posolda + */ +public class TransactionMongoStoreInvocationContext implements MongoStoreInvocationContext { + + // Assumption is that all objects has unique ID (unique across all the types) + private Map loadedObjects = new HashMap(); + + private Map> pendingUpdateTasks = new HashMap>(); + + private final MongoStore mongoStore; + + public TransactionMongoStoreInvocationContext(MongoStore mongoStore) { + this.mongoStore = mongoStore; + } + + @Override + public void addCreatedObject(MongoIdentifiableEntity entity) { + // For now just add it to list of loaded objects + addLoadedObject(entity); + } + + @Override + public void addLoadedObject(MongoIdentifiableEntity entity) { + loadedObjects.put(entity.getId(), entity); + } + + @Override + public T getLoadedObject(Class type, String id) { + return (T)loadedObjects.get(id); + } + + @Override + public void addUpdateTask(MongoIdentifiableEntity entityToUpdate, MongoTask task) { + if (!loadedObjects.containsValue(entityToUpdate)) { + throw new IllegalStateException("Entity " + entityToUpdate + " not found in loaded objects"); + } + + Set currentObjectTasks = pendingUpdateTasks.get(entityToUpdate); + if (currentObjectTasks == null) { + currentObjectTasks = new LinkedHashSet(); + pendingUpdateTasks.put(entityToUpdate, currentObjectTasks); + } else { + // if task is full update, then remove all other tasks as we need to do full update of object anyway + if (task.isFullUpdate()) { + currentObjectTasks.clear(); + } else { + // If it already contains task for fullUpdate, then we don't need to add ours as we need to do full update of object anyway + for (MongoTask current : currentObjectTasks) { + if (current.isFullUpdate()) { + return; + } + } + } + } + + currentObjectTasks.add(task); + } + + @Override + public void addRemovedObject(MongoIdentifiableEntity entityToRemove) { + // Remove all pending tasks and object from cache + pendingUpdateTasks.remove(entityToRemove); + loadedObjects.remove(entityToRemove.getId()); + + entityToRemove.afterRemove(this); + } + + @Override + public void beforeDBSearch(Class entityType) { + // Now execute pending update tasks of type, which will be searched + Set toRemove = new HashSet(); + + for (MongoIdentifiableEntity currentEntity : pendingUpdateTasks.keySet()) { + if (currentEntity.getClass().equals(entityType)) { + Set mongoTasks = pendingUpdateTasks.get(currentEntity); + for (MongoTask currentTask : mongoTasks) { + currentTask.execute(); + } + + toRemove.add(currentEntity); + } + } + + // Now remove all done tasks + for (MongoIdentifiableEntity entity : toRemove) { + pendingUpdateTasks.remove(entity); + } + } + + @Override + public void begin() { + loadedObjects.clear(); + pendingUpdateTasks.clear(); + } + + @Override + public void commit() { + // Now execute all pending update tasks + for (Set mongoTasks : pendingUpdateTasks.values()) { + for (MongoTask currentTask : mongoTasks) { + currentTask.execute(); + } + } + + // And clear it + loadedObjects.clear(); + pendingUpdateTasks.clear(); + } + + @Override + public void rollback() { + // Just clear the map without executions of tasks TODO: Attempt to do complete rollback (removal of created objects, restoring of removed objects, rollback of updates) + loadedObjects.clear(); + pendingUpdateTasks.clear(); + } + + @Override + public MongoStore getMongoStore() { + return mongoStore; + } +} 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 deleted file mode 100755 index 04824ba5cb..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBListConverter.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.keycloak.models.mongo.impl.types; - -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import org.keycloak.models.mongo.api.types.Converter; -import org.keycloak.models.mongo.api.types.TypeConverter; - -import java.util.ArrayList; - -/** - * @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/BasicDBListMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBListMapper.java new file mode 100755 index 0000000000..ff81604f60 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBListMapper.java @@ -0,0 +1,44 @@ +package org.keycloak.models.mongo.impl.types; + +import com.mongodb.BasicDBList; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; +import org.keycloak.models.mongo.api.types.MapperRegistry; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Marek Posolda + */ +public class BasicDBListMapper implements Mapper { + + private final MapperRegistry mapperRegistry; + + public BasicDBListMapper(MapperRegistry mapperRegistry) { + this.mapperRegistry = mapperRegistry; + } + + @Override + public List convertObject(MapperContext context) { + BasicDBList dbList = context.getObjectToConvert(); + ArrayList appObjects = new ArrayList(); + Class expectedListElementType = context.getGenericTypes().get(0); + + for (Object dbObject : dbList) { + MapperContext newContext = new MapperContext(dbObject, expectedListElementType, null); + appObjects.add(mapperRegistry.convertDBObjectToApplicationObject(newContext)); + } + return appObjects; + } + + @Override + public Class getTypeOfObjectToConvert() { + return BasicDBList.class; + } + + @Override + public Class getExpectedReturnType() { + return List.class; + } +} 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 deleted file mode 100644 index a423652b38..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectConverter.java +++ /dev/null @@ -1,102 +0,0 @@ -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/BasicDBObjectMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectMapper.java new file mode 100644 index 0000000000..bd423e07ad --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectMapper.java @@ -0,0 +1,124 @@ +package org.keycloak.models.mongo.impl.types; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import com.mongodb.BasicDBObject; +import org.jboss.logging.Logger; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoIdentifiableEntity; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; +import org.keycloak.models.mongo.api.types.MapperRegistry; +import org.keycloak.models.mongo.impl.MongoStoreImpl; +import org.keycloak.models.mongo.impl.EntityInfo; +import org.picketlink.common.properties.Property; +import org.picketlink.common.reflection.Types; + +/** + * @author Marek Posolda + */ +public class BasicDBObjectMapper implements Mapper { + + private static final Logger logger = Logger.getLogger(BasicDBObjectMapper.class); + + private final MongoStoreImpl mongoStoreImpl; + private final MapperRegistry mapperRegistry; + private final Class expectedObjectType; + + public BasicDBObjectMapper(MongoStoreImpl mongoStoreImpl, MapperRegistry mapperRegistry, Class expectedObjectType) { + this.mongoStoreImpl = mongoStoreImpl; + this.mapperRegistry = mapperRegistry; + this.expectedObjectType = expectedObjectType; + } + + @Override + public S convertObject(MapperContext context) { + BasicDBObject dbObject = context.getObjectToConvert(); + if (dbObject == null) { + return null; + } + + EntityInfo entityInfo = mongoStoreImpl.getEntityInfo(expectedObjectType); + + S object; + try { + object = expectedObjectType.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" + if (object instanceof MongoIdentifiableEntity) { + ((MongoIdentifiableEntity)object).setId(value.toString()); + } + + } else if ((property = entityInfo.getPropertyByName(key)) != null) { + // It's declared property with @DBField annotation + setPropertyValue(object, value, property); + + } else { + // Show warning if it's unknown + logger.warn("Property with key " + key + " not known for type " + expectedObjectType); + } + } + + return object; + } + + private void setPropertyValue(MongoEntity object, Object valueFromDB, Property property) { + if (valueFromDB == null) { + property.setValue(object, null); + return; + } + + MapperContext context; + + Type type = property.getBaseType(); + + // This can be the case when we have parameterized type (like "List") + if (type instanceof ParameterizedType) { + ParameterizedType parameterized = (ParameterizedType) type; + Type[] genericTypeArguments = parameterized.getActualTypeArguments(); + + List> genericTypes = new ArrayList>(); + for (Type genericType : genericTypeArguments) { + genericTypes.add((Class)genericType); + } + + Class expectedReturnType = (Class)parameterized.getRawType(); + context = new MapperContext(valueFromDB, expectedReturnType, genericTypes); + } else { + Class expectedReturnType = (Class)type; + // handle primitives + expectedReturnType = Types.boxedClass(expectedReturnType); + context = new MapperContext(valueFromDB, expectedReturnType, null); + } + + Object appObject = mapperRegistry.convertDBObjectToApplicationObject(context); + + if (Types.boxedClass(property.getJavaClass()).isAssignableFrom(appObject.getClass())) { + property.setValue(object, appObject); + } else { + throw new IllegalStateException("Converted object " + appObject + " is not of type " + context.getExpectedReturnType() + + ". So can't be assigned as property " + property.getName() + " of " + object.getClass()); + } + } + + @Override + public Class getTypeOfObjectToConvert() { + return BasicDBObject.class; + } + + @Override + public Class getExpectedReturnType() { + return expectedObjectType; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectToMapMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectToMapMapper.java new file mode 100644 index 0000000000..47e8d3cd48 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/BasicDBObjectToMapMapper.java @@ -0,0 +1,44 @@ +package org.keycloak.models.mongo.impl.types; + +import java.util.HashMap; +import java.util.Map; + +import com.mongodb.BasicDBObject; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; + +/** + * For now, we support just convert to Map + * + * @author Marek Posolda + */ +public class BasicDBObjectToMapMapper implements Mapper { + + @Override + public Map convertObject(MapperContext context) { + BasicDBObject objectToConvert = context.getObjectToConvert(); + + HashMap result = new HashMap(); + for (Map.Entry entry : objectToConvert.entrySet()) { + String key = entry.getKey(); + String value = (String)entry.getValue(); + + if (key.contains(MapMapper.DOT_PLACEHOLDER)) { + key = key.replaceAll(MapMapper.DOT_PLACEHOLDER, "."); + } + + result.put(key, value); + } + return result; + } + + @Override + public Class getTypeOfObjectToConvert() { + return BasicDBObject.class; + } + + @Override + public Class getExpectedReturnType() { + return Map.class; + } +} 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 deleted file mode 100644 index 891ccdd0e5..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ClassCache.java +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 2a800df46b..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/EnumToStringConverter.java +++ /dev/null @@ -1,26 +0,0 @@ -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/EnumToStringMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/EnumToStringMapper.java new file mode 100644 index 0000000000..a66633cf52 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/EnumToStringMapper.java @@ -0,0 +1,28 @@ +package org.keycloak.models.mongo.impl.types; + +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; + +/** + * @author Marek Posolda + */ +public class EnumToStringMapper implements Mapper { + + // 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(MapperContext context) { + Enum objectToConvert = context.getObjectToConvert(); + + return objectToConvert.toString(); + } + + @Override + public Class getTypeOfObjectToConvert() { + 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 deleted file mode 100755 index 49fb627f7a..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ListConverter.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.keycloak.models.mongo.impl.types; - -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import org.keycloak.models.mongo.api.types.Converter; -import org.keycloak.models.mongo.api.types.TypeConverter; - -import java.util.List; - -/** - * @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/ListMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ListMapper.java new file mode 100755 index 0000000000..7ac9a593e7 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/ListMapper.java @@ -0,0 +1,45 @@ +package org.keycloak.models.mongo.impl.types; + +import com.mongodb.BasicDBList; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; +import org.keycloak.models.mongo.api.types.MapperRegistry; + +import java.util.List; + +/** + * @author Marek Posolda + */ +public class ListMapper implements Mapper { + + private final MapperRegistry mapperRegistry; + private final Class listType; + + public ListMapper(MapperRegistry mapperRegistry, Class listType) { + this.mapperRegistry = mapperRegistry; + this.listType = listType; + } + + @Override + public BasicDBList convertObject(MapperContext context) { + T appObjectsList = context.getObjectToConvert(); + + BasicDBList dbObjects = new BasicDBList(); + for (Object appObject : appObjectsList) { + Object dbObject = mapperRegistry.convertApplicationObjectToDBObject(appObject, Object.class); + + dbObjects.add(dbObject); + } + return dbObjects; + } + + @Override + public Class getTypeOfObjectToConvert() { + return listType; + } + + @Override + public Class getExpectedReturnType() { + return BasicDBList.class; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/MapMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/MapMapper.java new file mode 100644 index 0000000000..4798ac1534 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/MapMapper.java @@ -0,0 +1,54 @@ +package org.keycloak.models.mongo.impl.types; + +import java.util.Map; +import java.util.Set; + +import com.mongodb.BasicDBObject; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; + +/** + * For now, we support just convert from Map + * + * @author Marek Posolda + */ +public class MapMapper implements Mapper { + + // Just some dummy way of encoding . character as it's not allowed by mongo in key fields + static final String DOT_PLACEHOLDER = "###"; + + private final Class mapType; + + public MapMapper(Class mapType) { + this.mapType = mapType; + } + + @Override + public BasicDBObject convertObject(MapperContext context) { + T objectToConvert = context.getObjectToConvert(); + + BasicDBObject dbObject = new BasicDBObject(); + Set entries = objectToConvert.entrySet(); + for (Map.Entry entry : entries) { + String key = (String)entry.getKey(); + String value = (String)entry.getValue(); + + if (key.contains(".")) { + key = key.replaceAll("\\.", DOT_PLACEHOLDER); + } + + dbObject.put(key, value); + } + return dbObject; + } + + @Override + public Class getTypeOfObjectToConvert() { + return mapType; + } + + @Override + public Class getExpectedReturnType() { + return BasicDBObject.class; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/MongoEntityMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/MongoEntityMapper.java new file mode 100755 index 0000000000..3514e6e6fe --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/MongoEntityMapper.java @@ -0,0 +1,58 @@ +package org.keycloak.models.mongo.impl.types; + +import com.mongodb.BasicDBObject; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; +import org.keycloak.models.mongo.api.types.MapperRegistry; +import org.keycloak.models.mongo.impl.MongoStoreImpl; +import org.keycloak.models.mongo.impl.EntityInfo; +import org.picketlink.common.properties.Property; + +import java.util.Collection; + +/** + * @author Marek Posolda + */ +public class MongoEntityMapper implements Mapper { + + private final MongoStoreImpl mongoStoreImpl; + private final MapperRegistry mapperRegistry; + private final Class expectedMongoEntityType; + + public MongoEntityMapper(MongoStoreImpl mongoStoreImpl, MapperRegistry mapperRegistry, Class expectedMongoEntityType) { + this.mongoStoreImpl = mongoStoreImpl; + this.mapperRegistry = mapperRegistry; + this.expectedMongoEntityType = expectedMongoEntityType; + } + + @Override + public BasicDBObject convertObject(MapperContext context) { + T applicationObject = context.getObjectToConvert(); + + EntityInfo entityInfo = mongoStoreImpl.getEntityInfo(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 = entityInfo.getProperties(); + for (Property property : props) { + String propName = property.getName(); + Object propValue = property.getValue(applicationObject); + + Object dbValue = propValue == null ? null : mapperRegistry.convertApplicationObjectToDBObject(propValue, Object.class); + dbObject.put(propName, dbValue); + } + + return dbObject; + } + + @Override + public Class getTypeOfObjectToConvert() { + return expectedMongoEntityType; + } + + @Override + public Class getExpectedReturnType() { + return BasicDBObject.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 deleted file mode 100755 index 35596a9671..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/NoSQLObjectConverter.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.keycloak.models.mongo.impl.types; - -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; - -import java.util.Collection; -import java.util.Map; - -/** - * @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/SimpleMapper.java similarity index 50% rename from model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/SimpleConverter.java rename to model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/SimpleMapper.java index 5ba1de5498..381cf6059a 100644 --- 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/SimpleMapper.java @@ -1,25 +1,29 @@ package org.keycloak.models.mongo.impl.types; -import org.keycloak.models.mongo.api.types.Converter; +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; /** + * Just returns input + * * @author Marek Posolda */ -public class SimpleConverter implements Converter { +public class SimpleMapper implements Mapper { private final Class expectedType; - public SimpleConverter(Class expectedType) { + public SimpleMapper(Class expectedType) { this.expectedType = expectedType; } @Override - public T convertObject(T objectToConvert) { + public T convertObject(MapperContext context) { + T objectToConvert = context.getObjectToConvert(); return objectToConvert; } @Override - public Class getConverterObjectType() { + public Class getTypeOfObjectToConvert() { 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 deleted file mode 100644 index 0c948eccb4..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/StringToEnumConverter.java +++ /dev/null @@ -1,32 +0,0 @@ -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/impl/types/StringToEnumMapper.java b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/StringToEnumMapper.java new file mode 100644 index 0000000000..08e558a580 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/impl/types/StringToEnumMapper.java @@ -0,0 +1,28 @@ +package org.keycloak.models.mongo.impl.types; + +import org.keycloak.models.mongo.api.types.Mapper; +import org.keycloak.models.mongo.api.types.MapperContext; + +/** + * @author Marek Posolda + */ +public class StringToEnumMapper implements Mapper { + + @Override + public Enum convertObject(MapperContext context) { + String enumValue = context.getObjectToConvert(); + + Class clazz = context.getExpectedReturnType(); + return Enum.valueOf(clazz, enumValue); + } + + @Override + public Class getTypeOfObjectToConvert() { + return String.class; + } + + @Override + public Class getExpectedReturnType() { + return Enum.class; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/MongoModelProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/MongoModelProvider.java index c035b5655d..910adde85a 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/MongoModelProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/MongoModelProvider.java @@ -2,6 +2,9 @@ package org.keycloak.models.mongo.keycloak; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.ModelProvider; +import org.keycloak.models.mongo.keycloak.adapters.MongoKeycloakSessionFactory; +import org.keycloak.models.mongo.utils.MongoConfiguration; +import org.keycloak.models.mongo.utils.SystemPropertiesConfigurationProvider; import java.lang.Override; @@ -18,16 +21,7 @@ public class MongoModelProvider implements ModelProvider { @Override public KeycloakSessionFactory createFactory() { - String host = PropertiesManager.getMongoHost(); - int port = PropertiesManager.getMongoPort(); - String dbName = PropertiesManager.getMongoDbName(); - boolean dropDatabaseOnStartup = PropertiesManager.dropDatabaseOnStartup(); - - // Create MongoDBSessionFactory via reflection now - try { - return new MongoDBSessionFactory(host, port, dbName, dropDatabaseOnStartup); - } catch (Exception e) { - throw new RuntimeException(e); - } + MongoConfiguration config = SystemPropertiesConfigurationProvider.createConfiguration(); + return new MongoKeycloakSessionFactory(config); } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/AbstractAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/AbstractAdapter.java new file mode 100644 index 0000000000..9e4f945cc7 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/AbstractAdapter.java @@ -0,0 +1,40 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoStore; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +/** + * @author Marek Posolda + */ +public abstract class AbstractAdapter { + + protected MongoStoreInvocationContext invocationContext; + + public AbstractAdapter(MongoStoreInvocationContext invocationContext) { + this.invocationContext = invocationContext; + } + + public abstract AbstractMongoIdentifiableEntity getMongoEntity(); + + @Override + public boolean equals(Object o) { + if (o == this) return true; + + if (o == null || getClass() != o.getClass()) return false; + + AbstractAdapter that = (AbstractAdapter) o; + + if (getMongoEntity() == null && that.getMongoEntity() == null) return true; + return getMongoEntity().equals(that.getMongoEntity()); + } + + @Override + public int hashCode() { + return getMongoEntity()!=null ? getMongoEntity().hashCode() : super.hashCode(); + } + + protected MongoStore getMongoStore() { + return invocationContext.getMongoStore(); + } +} 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 index c1a4dd2f6c..0686f4201c 100755 --- 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 @@ -1,13 +1,16 @@ package org.keycloak.models.mongo.keycloak.adapters; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; 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; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.ApplicationEntity; +import org.keycloak.models.mongo.keycloak.entities.RoleEntity; +import org.keycloak.models.mongo.keycloak.entities.UserEntity; +import org.keycloak.models.mongo.utils.MongoModelUtils; import java.util.ArrayList; import java.util.HashSet; @@ -17,31 +20,38 @@ import java.util.Set; /** * @author Marek Posolda */ -public class ApplicationAdapter implements ApplicationModel { +public class ApplicationAdapter extends AbstractAdapter implements ApplicationModel { - private final ApplicationData application; - private final NoSQL noSQL; + private final ApplicationEntity application; + private UserAdapter resourceUser; - private UserData resourceUser; + public ApplicationAdapter(ApplicationEntity applicationEntity, MongoStoreInvocationContext invContext) { + this(applicationEntity, null, invContext); + } - public ApplicationAdapter(ApplicationData applicationData, NoSQL noSQL) { - this.application = applicationData; - this.noSQL = noSQL; + public ApplicationAdapter(ApplicationEntity applicationEntity, UserAdapter resourceUser, MongoStoreInvocationContext invContext) { + super(invContext); + this.application = applicationEntity; + this.resourceUser = resourceUser; } @Override public void updateApplication() { - noSQL.saveObject(application); + getMongoStore().updateEntity(application, invocationContext); } @Override - public UserModel getApplicationUser() { + public UserAdapter 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()); + UserEntity userEntity = getMongoStore().loadEntity(UserEntity.class, application.getResourceUserId(), invocationContext); + if (userEntity == null) { + throw new IllegalStateException("User " + application.getResourceUserId() + " not found"); + } + resourceUser = new UserAdapter(userEntity, invocationContext); } - return resourceUser != null ? new UserAdapter(resourceUser, noSQL) : null; + return resourceUser; } @Override @@ -101,182 +111,90 @@ public class ApplicationAdapter implements ApplicationModel { @Override public RoleAdapter getRole(String name) { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("name", name) - .andCondition("applicationId", getId()) - .build(); - RoleData role = noSQL.loadSingleObject(RoleData.class, query); + DBObject query = new QueryBuilder() + .and("name").is(name) + .and("applicationId").is(getId()) + .get(); + RoleEntity role = getMongoStore().loadSingleEntity(RoleEntity.class, query, invocationContext); if (role == null) { return null; } else { - return new RoleAdapter(role, noSQL); + return new RoleAdapter(role, invocationContext); } } @Override public RoleModel getRoleById(String id) { - RoleData role = noSQL.loadObject(RoleData.class, id); + RoleEntity role = getMongoStore().loadEntity(RoleEntity.class, id, invocationContext); if (role == null) { return null; } else { - return new RoleAdapter(role, noSQL); + return new RoleAdapter(role, this, invocationContext); } } - @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"); + RoleAdapter existing = getRole(name); + if (existing != null) { + return existing; } - RoleData roleData = new RoleData(); - roleData.setName(name); - roleData.setApplicationId(getId()); + RoleEntity roleEntity = new RoleEntity(); + roleEntity.setName(name); + roleEntity.setApplicationId(getId()); - noSQL.saveObject(roleData); - return new RoleAdapter(roleData, noSQL); + getMongoStore().insertEntity(roleEntity, invocationContext); + return new RoleAdapter(roleEntity, this, invocationContext); } @Override - public List getRoles() { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("applicationId", getId()) - .build(); - List roles = noSQL.loadObjects(RoleData.class, query); + public boolean removeRoleById(String id) { + return getMongoStore().removeEntity(RoleEntity.class, id, invocationContext); + } - List result = new ArrayList(); - for (RoleData role : roles) { - result.add(new RoleAdapter(role, noSQL)); + @Override + public Set getRoles() { + DBObject query = new QueryBuilder() + .and("applicationId").is(getId()) + .get(); + List roles = getMongoStore().loadEntities(RoleEntity.class, query, invocationContext); + + Set result = new HashSet(); + for (RoleEntity role : roles) { + result.add(new RoleAdapter(role, this, invocationContext)); } 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) { + public Set getApplicationRoleMappings(UserModel user) { + Set result = new HashSet(); + List roles = MongoModelUtils.getAllRolesOfUser(user, invocationContext); + + for (RoleEntity role : roles) { if (getId().equals(role.getApplicationId())) { - result.add(role.getName()); + result.add(new RoleAdapter(role, this, invocationContext)); } } 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) { + public void addScope(RoleModel role) { + UserAdapter appUser = getApplicationUser(); + getMongoStore().pushItemToList(appUser.getUser(), "scopeIds", role.getId(), true, invocationContext); + } + + @Override + public Set getApplicationScopeMappings(UserModel user) { + Set result = new HashSet(); + List roles = MongoModelUtils.getAllScopesOfUser(user, invocationContext); + + for (RoleEntity 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)); + result.add(new RoleAdapter(role, this, invocationContext)); } } return result; @@ -284,16 +202,36 @@ public class ApplicationAdapter implements ApplicationModel { @Override public List getDefaultRoles() { - return null; //To change body of implemented methods use File | Settings | File Templates. + return application.getDefaultRoles(); } @Override public void addDefaultRole(String name) { - //To change body of implemented methods use File | Settings | File Templates. + RoleModel role = getRole(name); + if (role == null) { + addRole(name); + } + + getMongoStore().pushItemToList(application, "defaultRoles", name, true, invocationContext); } @Override public void updateDefaultRoles(String[] defaultRoles) { - //To change body of implemented methods use File | Settings | File Templates. + List roleNames = new ArrayList(); + for (String roleName : defaultRoles) { + RoleModel role = getRole(roleName); + if (role == null) { + addRole(roleName); + } + + roleNames.add(roleName); + } + + application.setDefaultRoles(roleNames); + } + + @Override + public AbstractMongoIdentifiableEntity getMongoEntity() { + return application; } } 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 deleted file mode 100755 index f964605cb8..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoDBSessionFactory.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.keycloak.models.mongo.keycloak.adapters; - -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.impl.MongoDBImpl; -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.keycloak.data.credentials.OTPData; -import org.keycloak.models.mongo.keycloak.data.credentials.PasswordData; - -import java.net.UnknownHostException; - -/** - * 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/MongoKeycloakSession.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSession.java new file mode 100755 index 0000000000..a57916b466 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSession.java @@ -0,0 +1,100 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +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.MongoStore; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.impl.context.TransactionMongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.RealmEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Marek Posolda + */ +public class MongoKeycloakSession implements KeycloakSession { + + private final MongoStoreInvocationContext invocationContext; + private final MongoKeycloakTransaction transaction; + + public MongoKeycloakSession(MongoStore mongoStore) { + // this.invocationContext = new SimpleMongoStoreInvocationContext(mongoStore); + this.invocationContext = new TransactionMongoStoreInvocationContext(mongoStore); + this.transaction = new MongoKeycloakTransaction(invocationContext); + } + + @Override + public KeycloakTransaction getTransaction() { + return transaction; + } + + @Override + public void close() { + // TODO + } + + @Override + public RealmModel createRealm(String name) { + return createRealm(KeycloakModelUtils.generateId(), name); + } + + @Override + public RealmModel createRealm(String id, String name) { + if (getRealm(id) != null) { + throw new IllegalStateException("Realm with id '" + id + "' already exists"); + } + + RealmEntity newRealm = new RealmEntity(); + newRealm.setId(id); + newRealm.setName(name); + + getMongoStore().insertEntity(newRealm, invocationContext); + + return new RealmAdapter(newRealm, invocationContext); + } + + @Override + public RealmModel getRealm(String id) { + RealmEntity realmEntity = getMongoStore().loadEntity(RealmEntity.class, id, invocationContext); + return realmEntity != null ? new RealmAdapter(realmEntity, invocationContext) : null; + } + + @Override + public List getRealms(UserModel admin) { + DBObject query = new BasicDBObject(); + List realms = getMongoStore().loadEntities(RealmEntity.class, query, invocationContext); + + List results = new ArrayList(); + for (RealmEntity realmEntity : realms) { + results.add(new RealmAdapter(realmEntity, invocationContext)); + } + return results; + } + + @Override + public RealmModel getRealmByName(String name) { + DBObject query = new QueryBuilder() + .and("name").is(name) + .get(); + RealmEntity realm = getMongoStore().loadSingleEntity(RealmEntity.class, query, invocationContext); + + if (realm == null) return null; + return new RealmAdapter(realm, invocationContext); + } + + @Override + public boolean removeRealm(String id) { + return getMongoStore().removeEntity(RealmEntity.class, id, invocationContext); + } + + protected MongoStore getMongoStore() { + return invocationContext.getMongoStore(); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java new file mode 100755 index 0000000000..7ff4ead8d2 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSessionFactory.java @@ -0,0 +1,70 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +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.MongoEntity; +import org.keycloak.models.mongo.api.MongoStore; +import org.keycloak.models.mongo.impl.MongoStoreImpl; +import org.keycloak.models.mongo.keycloak.entities.ApplicationEntity; +import org.keycloak.models.mongo.keycloak.entities.CredentialEntity; +import org.keycloak.models.mongo.keycloak.entities.OAuthClientEntity; +import org.keycloak.models.mongo.keycloak.entities.RealmEntity; +import org.keycloak.models.mongo.keycloak.entities.RequiredCredentialEntity; +import org.keycloak.models.mongo.keycloak.entities.RoleEntity; +import org.keycloak.models.mongo.keycloak.entities.SocialLinkEntity; +import org.keycloak.models.mongo.keycloak.entities.UserEntity; +import org.keycloak.models.mongo.utils.MongoConfiguration; + +import java.net.UnknownHostException; + +/** + * KeycloakSessionFactory implementation based on MongoDB + * + * @author Marek Posolda + */ +public class MongoKeycloakSessionFactory implements KeycloakSessionFactory { + protected static final Logger logger = Logger.getLogger(MongoKeycloakSessionFactory.class); + + private static final Class[] MANAGED_ENTITY_TYPES = (Class[])new Class[] { + RealmEntity.class, + UserEntity.class, + RoleEntity.class, + RequiredCredentialEntity.class, + CredentialEntity.class, + SocialLinkEntity.class, + ApplicationEntity.class, + OAuthClientEntity.class + }; + + private final MongoClient mongoClient; + private final MongoStore mongoStore; + + public MongoKeycloakSessionFactory(MongoConfiguration config) { + logger.info(String.format("Configuring MongoStore with: " + config)); + + try { + // TODO: authentication support + mongoClient = new MongoClient(config.getHost(), config.getPort()); + + DB db = mongoClient.getDB(config.getDbName()); + mongoStore = new MongoStoreImpl(db, config.isClearCollectionsOnStartup(), MANAGED_ENTITY_TYPES); + + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + @Override + public KeycloakSession createSession() { + return new MongoKeycloakSession(mongoStore); + } + + @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/MongoKeycloakTransaction.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakTransaction.java new file mode 100644 index 0000000000..c9d9d37634 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakTransaction.java @@ -0,0 +1,60 @@ +package org.keycloak.models.mongo.keycloak.adapters; + +import org.keycloak.models.KeycloakTransaction; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +/** + * @author Marek Posolda + */ +public class MongoKeycloakTransaction implements KeycloakTransaction { + + private final MongoStoreInvocationContext invocationContext; + + private boolean started = false; + private boolean rollbackOnly = false; + + public MongoKeycloakTransaction(MongoStoreInvocationContext invocationContext) { + this.invocationContext = invocationContext; + } + + @Override + public void begin() { + if (started) { + throw new IllegalStateException("Transaction already started"); + } + started = true; + invocationContext.begin(); + } + + @Override + public void commit() { + if (!started) { + throw new IllegalStateException("Transaction not yet started"); + } + if (rollbackOnly) { + throw new IllegalStateException("Can't commit as transaction marked for rollback"); + } + + invocationContext.commit(); + } + + @Override + public void rollback() { + invocationContext.rollback(); + } + + @Override + public void setRollbackOnly() { + this.rollbackOnly = true; + } + + @Override + public boolean getRollbackOnly() { + return rollbackOnly; + } + + @Override + public boolean isActive() { + return started; + } +} 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 deleted file mode 100755 index bbd5ea4e61..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLSession.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.keycloak.models.mongo.keycloak.adapters; - -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.NoSQL; -import org.keycloak.models.mongo.api.query.NoSQLQuery; -import org.keycloak.models.mongo.keycloak.data.RealmData; -import org.keycloak.models.utils.KeycloakSessionUtils; - -import java.util.ArrayList; -import java.util.List; - -/** - * @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 deleted file mode 100644 index 3d166357ae..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/NoSQLTransaction.java +++ /dev/null @@ -1,39 +0,0 @@ -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 index d522db9b9c..2eacc45165 100755 --- 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 @@ -2,28 +2,27 @@ 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; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.OAuthClientEntity; +import org.keycloak.models.mongo.keycloak.entities.UserEntity; /** * @author Marek Posolda */ -public class OAuthClientAdapter implements OAuthClientModel { +public class OAuthClientAdapter extends AbstractAdapter implements OAuthClientModel { - private final OAuthClientData delegate; + private final OAuthClientEntity delegate; private UserAdapter oauthAgent; - private final NoSQL noSQL; - public OAuthClientAdapter(OAuthClientData oauthClientData, UserAdapter oauthAgent, NoSQL noSQL) { - this.delegate = oauthClientData; + public OAuthClientAdapter(OAuthClientEntity oauthClientEntity, UserAdapter oauthAgent, MongoStoreInvocationContext invContext) { + super(invContext); + this.delegate = oauthClientEntity; this.oauthAgent = oauthAgent; - this.noSQL = noSQL; } - public OAuthClientAdapter(OAuthClientData oauthClientData, NoSQL noSQL) { - this.delegate = oauthClientData; - this.noSQL = noSQL; + public OAuthClientAdapter(OAuthClientEntity oauthClientEntity, MongoStoreInvocationContext invContext) { + this(oauthClientEntity, null, invContext); } @Override @@ -35,10 +34,14 @@ public class OAuthClientAdapter implements OAuthClientModel { 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; + UserEntity user = getMongoStore().loadEntity(UserEntity.class, delegate.getOauthAgentId(), invocationContext); + oauthAgent = user!=null ? new UserAdapter(user, invocationContext) : null; } return oauthAgent; } + @Override + public AbstractMongoIdentifiableEntity getMongoEntity() { + return 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 index 7d4aa72690..6083eeb589 100755 --- 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 @@ -1,63 +1,61 @@ package org.keycloak.models.mongo.keycloak.adapters; -import org.bouncycastle.openssl.PEMWriter; -import org.keycloak.PemUtils; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.jboss.logging.Logger; import org.keycloak.models.ApplicationModel; import org.keycloak.models.OAuthClientModel; +import org.keycloak.models.PasswordPolicy; 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.NoSQL; -import org.keycloak.models.mongo.api.query.NoSQLQuery; -import org.keycloak.models.mongo.api.query.NoSQLQueryBuilder; -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.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.representations.idm.CredentialRepresentation; -import org.picketlink.idm.credential.Credentials; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.ApplicationEntity; +import org.keycloak.models.mongo.keycloak.entities.CredentialEntity; +import org.keycloak.models.mongo.keycloak.entities.OAuthClientEntity; +import org.keycloak.models.mongo.keycloak.entities.RealmEntity; +import org.keycloak.models.mongo.keycloak.entities.RequiredCredentialEntity; +import org.keycloak.models.mongo.keycloak.entities.RoleEntity; +import org.keycloak.models.mongo.keycloak.entities.SocialLinkEntity; +import org.keycloak.models.mongo.keycloak.entities.UserEntity; +import org.keycloak.models.mongo.utils.MongoModelUtils; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.Pbkdf2PasswordEncoder; +import org.keycloak.models.utils.TimeBasedOTP; -import java.io.IOException; -import java.io.StringWriter; import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; /** * @author Marek Posolda */ -public class RealmAdapter implements RealmModel { +public class RealmAdapter extends AbstractAdapter implements RealmModel { - private final RealmData realm; - private final NoSQL noSQL; + private static final Logger logger = Logger.getLogger(RealmAdapter.class); + + private final RealmEntity realm; 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()); + private volatile transient PasswordPolicy passwordPolicy; - public RealmAdapter(RealmData realmData, NoSQL noSQL) { - this.realm = realmData; - this.noSQL = noSQL; - } - - protected String getOid() { - return realm.getOid(); + public RealmAdapter(RealmEntity realmEntity, MongoStoreInvocationContext invocationContext) { + super(invocationContext); + this.realm = realmEntity; } @Override @@ -87,28 +85,6 @@ public class RealmAdapter implements RealmModel { 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(); @@ -120,17 +96,6 @@ public class RealmAdapter implements RealmModel { 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(); @@ -164,6 +129,43 @@ public class RealmAdapter implements RealmModel { updateRealm(); } + @Override + public boolean isSocial() { + return realm.isSocial(); + } + + @Override + public void setSocial(boolean social) { + realm.setSocial(social); + updateRealm(); + } + + @Override + public boolean isUpdateProfileOnInitialSocialLogin() { + return realm.isUpdateProfileOnInitialSocialLogin(); + } + + @Override + public void setUpdateProfileOnInitialSocialLogin(boolean updateProfileOnInitialSocialLogin) { + realm.setUpdateProfileOnInitialSocialLogin(updateProfileOnInitialSocialLogin); + updateRealm(); + } + + @Override + public PasswordPolicy getPasswordPolicy() { + if (passwordPolicy == null) { + passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy()); + } + return passwordPolicy; + } + + @Override + public void setPasswordPolicy(PasswordPolicy policy) { + this.passwordPolicy = policy; + realm.setPasswordPolicy(policy.toString()); + updateRealm(); + } + @Override public int getTokenLifespan() { return realm.getTokenLifespan(); @@ -224,197 +226,238 @@ public class RealmAdapter implements RealmModel { @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); - } - } + publicKey = KeycloakModelUtils.getPublicKey(getPublicKeyPem()); 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)); + String publicKeyPem = KeycloakModelUtils.getPemFromKey(publicKey); + setPublicKeyPem(publicKeyPem); } @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); - } - } + privateKey = KeycloakModelUtils.getPrivateKey(getPrivateKeyPem()); 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)); + String privateKeyPem = KeycloakModelUtils.getPemFromKey(privateKey); + setPrivateKeyPem(privateKeyPem); + } + + @Override + public String getLoginTheme() { + return realm.getLoginTheme(); + } + + @Override + public void setLoginTheme(String name) { + realm.setLoginTheme(name); + updateRealm(); + } + + @Override + public String getAccountTheme() { + return realm.getAccountTheme(); + } + + @Override + public void setAccountTheme(String name) { + realm.setAccountTheme(name); + updateRealm(); } @Override public UserAdapter getUser(String name) { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("loginName", name) - .andCondition("realmId", getOid()) - .build(); - UserData user = noSQL.loadSingleObject(UserData.class, query); + DBObject query = new QueryBuilder() + .and("loginName").is(name) + .and("realmId").is(getId()) + .get(); + UserEntity user = getMongoStore().loadSingleEntity(UserEntity.class, query, invocationContext); if (user == null) { return null; } else { - return new UserAdapter(user, noSQL); + return new UserAdapter(user, invocationContext); + } + } + + @Override + public UserModel getUserByEmail(String email) { + DBObject query = new QueryBuilder() + .and("email").is(email) + .and("realmId").is(getId()) + .get(); + UserEntity user = getMongoStore().loadSingleEntity(UserEntity.class, query, invocationContext); + + if (user == null) { + return null; + } else { + return new UserAdapter(user, invocationContext); } } @Override public UserAdapter addUser(String username) { + UserAdapter userModel = addUserEntity(username); + + for (String r : getDefaultRoles()) { + grantRole(userModel, getRole(r)); + } + + for (ApplicationModel application : getApplications()) { + for (String r : application.getDefaultRoles()) { + grantRole(userModel, application.getRole(r)); + } + } + + return userModel; + } + + // Add just user entity without defaultRoles + protected UserAdapter addUserEntity(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()); + UserEntity userEntity = new UserEntity(); + userEntity.setLoginName(username); + userEntity.setEnabled(true); + userEntity.setRealmId(getId()); - noSQL.saveObject(userData); - return new UserAdapter(userData, noSQL); + getMongoStore().insertEntity(userEntity, invocationContext); + return new UserAdapter(userEntity, invocationContext); } - // 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 boolean removeUser(String name) { + DBObject query = new QueryBuilder() + .and("loginName").is(name) + .and("realmId").is(getId()) + .get(); + return getMongoStore().removeEntities(UserEntity.class, query, invocationContext); } @Override public RoleAdapter getRole(String name) { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("name", name) - .andCondition("realmId", getOid()) - .build(); - RoleData role = noSQL.loadSingleObject(RoleData.class, query); + DBObject query = new QueryBuilder() + .and("name").is(name) + .and("realmId").is(getId()) + .get(); + RoleEntity role = getMongoStore().loadSingleEntity(RoleEntity.class, query, invocationContext); if (role == null) { return null; } else { - return new RoleAdapter(role, noSQL); + return new RoleAdapter(role, this, invocationContext); } } @Override public RoleModel addRole(String name) { - if (getRole(name) != null) { - throw new IllegalArgumentException("Role " + name + " already exists"); + RoleAdapter role = getRole(name); + if (role != null) { + // Compatibility with JPA model + return role; + // throw new IllegalArgumentException("Role " + name + " already exists"); } - RoleData roleData = new RoleData(); - roleData.setName(name); - roleData.setRealmId(getOid()); + RoleEntity roleEntity = new RoleEntity(); + roleEntity.setName(name); + roleEntity.setRealmId(getId()); - noSQL.saveObject(roleData); - return new RoleAdapter(roleData, noSQL); + getMongoStore().insertEntity(roleEntity, invocationContext); + return new RoleAdapter(roleEntity, this, invocationContext); } @Override - public List getRoles() { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("realmId", getOid()) - .build(); - List roles = noSQL.loadObjects(RoleData.class, query); + public boolean removeRoleById(String id) { + return getMongoStore().removeEntity(RoleEntity.class, id, invocationContext); + } - List result = new ArrayList(); - for (RoleData role : roles) { - result.add(new RoleAdapter(role, noSQL)); + @Override + public Set getRoles() { + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .get(); + List roles = getMongoStore().loadEntities(RoleEntity.class, query, invocationContext); + + Set result = new HashSet(); + + if (roles == null) return result; + for (RoleEntity role : roles) { + result.add(new RoleAdapter(role, this, invocationContext)); } 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)); + public RoleModel getRoleById(String id) { + RoleEntity role = getMongoStore().loadEntity(RoleEntity.class, id, invocationContext); + if (role == null) { + return null; + } else { + return new RoleAdapter(role, this, invocationContext); } - return defaultRoleModels; + } + + @Override + public List getDefaultRoles() { + return realm.getDefaultRoles(); } @Override public void addDefaultRole(String name) { RoleModel role = getRole(name); if (role == null) { - role = addRole(name); + addRole(name); } - noSQL.pushItemToList(realm, "defaultRoles", role.getId()); + getMongoStore().pushItemToList(realm, "defaultRoles", name, true, invocationContext); } @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(); + List roleNames = new ArrayList(); for (String roleName : defaultRoles) { RoleModel role = getRole(roleName); if (role == null) { - role = addRole(roleName); + addRole(roleName); } - roleIds.add(role.getId()); + roleNames.add(roleName); } - realm.setDefaultRoles(roleIds); + realm.setDefaultRoles(roleNames); updateRealm(); } @Override public ApplicationModel getApplicationById(String id) { - ApplicationData appData = noSQL.loadObject(ApplicationData.class, id); + ApplicationEntity appData = getMongoStore().loadEntity(ApplicationEntity.class, id, invocationContext); // Check if application belongs to this realm - if (appData == null || !getOid().equals(appData.getRealmId())) { + if (appData == null || !getId().equals(appData.getRealmId())) { return null; } - ApplicationModel model = new ApplicationAdapter(appData, noSQL); - return model; + return new ApplicationAdapter(appData, invocationContext); + } + + @Override + public ApplicationModel getApplicationByName(String name) { + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .and("name").is(name) + .get(); + ApplicationEntity appEntity = getMongoStore().loadSingleEntity(ApplicationEntity.class, query, invocationContext); + return appEntity==null ? null : new ApplicationAdapter(appEntity, invocationContext); } @Override @@ -428,356 +471,355 @@ public class RealmAdapter implements RealmModel { @Override public List getApplications() { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("realmId", getOid()) - .build(); - List appDatas = noSQL.loadObjects(ApplicationData.class, query); + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .get(); + List appDatas = getMongoStore().loadEntities(ApplicationEntity.class, query, invocationContext); List result = new ArrayList(); - for (ApplicationData appData : appDatas) { - result.add(new ApplicationAdapter(appData, noSQL)); + for (ApplicationEntity appData : appDatas) { + result.add(new ApplicationAdapter(appData, invocationContext)); } return result; } @Override public ApplicationModel addApplication(String name) { - UserAdapter resourceUser = addUser(name); + UserAdapter resourceUser = addUserEntity(name); - ApplicationData appData = new ApplicationData(); + ApplicationEntity appData = new ApplicationEntity(); appData.setName(name); - appData.setRealmId(getOid()); + appData.setRealmId(getId()); + appData.setEnabled(true); appData.setResourceUserId(resourceUser.getUser().getId()); - noSQL.saveObject(appData); + getMongoStore().insertEntity(appData, invocationContext); - ApplicationModel resource = new ApplicationAdapter(appData, noSQL); - return resource; + return new ApplicationAdapter(appData, resourceUser, invocationContext); + } + + @Override + public boolean removeApplication(String id) { + return getMongoStore().removeEntity(ApplicationEntity.class, id, invocationContext); } @Override public boolean hasRole(UserModel user, RoleModel role) { - UserData userData = ((UserAdapter)user).getUser(); + Set roles = getRoleMappings(user); + if (roles.contains(role)) return true; - List roleIds = userData.getRoleIds(); - String roleId = role.getId(); - if (roleIds != null) { - for (String currentId : roleIds) { - if (roleId.equals(currentId)) { - return true; - } - } + for (RoleModel mapping : roles) { + if (mapping.hasRole(role)) return true; } return false; } @Override public void grantRole(UserModel user, RoleModel role) { - UserData userData = ((UserAdapter)user).getUser(); - noSQL.pushItemToList(userData, "roleIds", role.getId()); + UserEntity userEntity = ((UserAdapter)user).getUser(); + getMongoStore().pushItemToList(userEntity, "roleIds", role.getId(), true, invocationContext); } @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)); + public Set getRoleMappings(UserModel user) { + Set result = new HashSet(); + List roles = MongoModelUtils.getAllRolesOfUser(user, invocationContext); + + for (RoleEntity role : roles) { + if (getId().equals(role.getRealmId())) { + result.add(new RoleAdapter(role, this, invocationContext)); + } else { + // Likely applicationRole, but we don't have this application yet + result.add(new RoleAdapter(role, invocationContext)); } } 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()); + public Set getRealmRoleMappings(UserModel user) { + Set allRoles = getRoleMappings(user); + + // Filter to retrieve just realm roles TODO: Maybe improve to avoid filter programmatically... Maybe have separate fields for realmRoles and appRoles on user? + Set realmRoles = new HashSet(); + for (RoleModel role : allRoles) { + RoleEntity roleEntity = ((RoleAdapter)role).getRole(); + + if (getId().equals(roleEntity.getRealmId())) { + realmRoles.add(role); } } - return result; + return realmRoles; } @Override public void deleteRoleMapping(UserModel user, RoleModel role) { - UserData userData = ((UserAdapter)user).getUser(); - noSQL.pullItemFromList(userData, "roleIds", role.getId()); + UserEntity userEntity = ((UserAdapter)user).getUser(); + getMongoStore().pullItemFromList(userEntity, "roleIds", role.getId(), invocationContext); } @Override - public void addScopeMapping(UserModel agent, String roleName) { - RoleAdapter role = getRole(roleName); - if (role == null) { - throw new RuntimeException("Role not found"); - } + public Set getScopeMappings(UserModel user) { + Set result = new HashSet(); + List roles = MongoModelUtils.getAllScopesOfUser(user, invocationContext); - addScopeMapping(agent, role); + for (RoleEntity role : roles) { + if (getId().equals(role.getRealmId())) { + result.add(new RoleAdapter(role, this, invocationContext)); + } else { + // Likely applicationRole, but we don't have this application yet + result.add(new RoleAdapter(role, invocationContext)); + } + } + return result; + } + + @Override + public Set getRealmScopeMappings(UserModel user) { + Set allScopes = getScopeMappings(user); + + // Filter to retrieve just realm roles TODO: Maybe improve to avoid filter programmatically... Maybe have separate fields for realmRoles and appRoles on user? + Set realmRoles = new HashSet(); + for (RoleModel role : allScopes) { + RoleEntity roleEntity = ((RoleAdapter)role).getRole(); + + if (getId().equals(roleEntity.getRealmId())) { + realmRoles.add(role); + } + } + return realmRoles; } @Override public void addScopeMapping(UserModel agent, RoleModel role) { - UserData userData = ((UserAdapter)agent).getUser(); - noSQL.pushItemToList(userData, "scopeIds", role.getId()); + UserEntity userEntity = ((UserAdapter)agent).getUser(); + getMongoStore().pushItemToList(userEntity, "scopeIds", role.getId(), true, invocationContext); } @Override public void deleteScopeMapping(UserModel user, RoleModel role) { - UserData userData = ((UserAdapter)user).getUser(); - noSQL.pullItemFromList(userData, "scopeIds", role.getId()); + UserEntity userEntity = ((UserAdapter)user).getUser(); + getMongoStore().pullItemFromList(userEntity, "scopeIds", role.getId(), invocationContext); } @Override public OAuthClientModel addOAuthClient(String name) { - UserAdapter oauthAgent = addUser(name); + UserAdapter oauthAgent = addUserEntity(name); - OAuthClientData oauthClient = new OAuthClientData(); + OAuthClientEntity oauthClient = new OAuthClientEntity(); oauthClient.setOauthAgentId(oauthAgent.getUser().getId()); - oauthClient.setRealmId(getOid()); - noSQL.saveObject(oauthClient); + oauthClient.setRealmId(getId()); + oauthClient.setName(name); + getMongoStore().insertEntity(oauthClient, invocationContext); - return new OAuthClientAdapter(oauthClient, oauthAgent, noSQL); + return new OAuthClientAdapter(oauthClient, oauthAgent, invocationContext); + } + + @Override + public boolean removeOAuthClient(String id) { + return getMongoStore().removeEntity(OAuthClientEntity.class, id, invocationContext); } @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); + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .and("oauthAgentId").is(user.getUser().getId()) + .get(); + OAuthClientEntity oauthClient = getMongoStore().loadSingleEntity(OAuthClientEntity.class, query, invocationContext); + return oauthClient == null ? null : new OAuthClientAdapter(oauthClient, user, invocationContext); + } + + @Override + public OAuthClientModel getOAuthClientById(String id) { + OAuthClientEntity clientEntity = getMongoStore().loadEntity(OAuthClientEntity.class, id, invocationContext); + if (clientEntity == null) return null; + return new OAuthClientAdapter(clientEntity, invocationContext); } @Override public List getOAuthClients() { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("realmId", getOid()) - .build(); - List results = noSQL.loadObjects(OAuthClientData.class, query); + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .get(); + List results = getMongoStore().loadEntities(OAuthClientEntity.class, query, invocationContext); List list = new ArrayList(); - for (OAuthClientData data : results) { - list.add(new OAuthClientAdapter(data, noSQL)); + for (OAuthClientEntity data : results) { + list.add(new OAuthClientAdapter(data, invocationContext)); } 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 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); + public void addRequiredCredential(String type) { + RequiredCredentialModel credentialModel = initRequiredCredentialModel(type); + addRequiredCredential(credentialModel, realm.getRequiredCredentials()); } @Override public void addRequiredResourceCredential(String type) { RequiredCredentialModel credentialModel = initRequiredCredentialModel(type); - addRequiredCredential(credentialModel, RequiredCredentialData.CLIENT_TYPE_RESOURCE); + addRequiredCredential(credentialModel, realm.getRequiredApplicationCredentials()); } @Override public void addRequiredOAuthClientCredential(String type) { RequiredCredentialModel credentialModel = initRequiredCredentialModel(type); - addRequiredCredential(credentialModel, RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE); + addRequiredCredential(credentialModel, realm.getRequiredOAuthClientCredentials()); } - 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()); + protected void addRequiredCredential(RequiredCredentialModel credentialModel, List persistentCollection) { + RequiredCredentialEntity credEntity = new RequiredCredentialEntity(); + credEntity.setType(credentialModel.getType()); + credEntity.setFormLabel(credentialModel.getFormLabel()); + credEntity.setInput(credentialModel.isInput()); + credEntity.setSecret(credentialModel.isSecret()); - credData.setRealmId(getOid()); - credData.setClientType(clientType); + persistentCollection.add(credEntity); - noSQL.saveObject(credData); + updateRealm(); } @Override public void updateRequiredCredentials(Set creds) { - List credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_USER); - updateRequiredCredentials(creds, credsData); + updateRequiredCredentials(creds, realm.getRequiredCredentials()); } @Override public void updateRequiredApplicationCredentials(Set creds) { - List credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_RESOURCE); - updateRequiredCredentials(creds, credsData); + updateRequiredCredentials(creds, realm.getRequiredApplicationCredentials()); } @Override public void updateRequiredOAuthClientCredentials(Set creds) { - List credsData = getRequiredCredentialsData(RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE); - updateRequiredCredentials(creds, credsData); + updateRequiredCredentials(creds, realm.getRequiredOAuthClientCredentials()); } - protected void updateRequiredCredentials(Set creds, List credsData) { + protected void updateRequiredCredentials(Set creds, List credsEntities) { Set already = new HashSet(); - for (RequiredCredentialData data : credsData) { - if (!creds.contains(data.getType())) { - noSQL.removeObject(data); + Set toRemove = new HashSet(); + for (RequiredCredentialEntity entity : credsEntities) { + if (!creds.contains(entity.getType())) { + toRemove.add(entity); } else { - already.add(data.getType()); + already.add(entity.getType()); } } + for (RequiredCredentialEntity entity : toRemove) { + credsEntities.remove(entity); + } for (String cred : creds) { - // TODO - System.out.println("updating cred: " + cred); - // logger.info("updating cred: " + cred); + logger.info("updating cred: " + cred); if (!already.contains(cred)) { - addRequiredCredential(cred); + RequiredCredentialModel credentialModel = initRequiredCredentialModel(cred); + addRequiredCredential(credentialModel, credsEntities); } } } @Override public List getRequiredCredentials() { - return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_USER); + return convertRequiredCredentialEntities(realm.getRequiredCredentials()); } @Override public List getRequiredApplicationCredentials() { - return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_RESOURCE); + return convertRequiredCredentialEntities(realm.getRequiredApplicationCredentials()); } @Override public List getRequiredOAuthClientCredentials() { - return getRequiredCredentials(RequiredCredentialData.CLIENT_TYPE_OAUTH_RESOURCE); + return convertRequiredCredentialEntities(realm.getRequiredOAuthClientCredentials()); } - protected List getRequiredCredentials(int credentialType) { - List credsData = getRequiredCredentialsData(credentialType); + protected List convertRequiredCredentialEntities(Collection credEntities) { List result = new ArrayList(); - for (RequiredCredentialData data : credsData) { + for (RequiredCredentialEntity entity : credEntities) { RequiredCredentialModel model = new RequiredCredentialModel(); - model.setFormLabel(data.getFormLabel()); - model.setInput(data.isInput()); - model.setSecret(data.isSecret()); - model.setType(data.getType()); + model.setFormLabel(entity.getFormLabel()); + model.setInput(entity.isInput()); + model.setSecret(entity.isSecret()); + model.setType(entity.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; + for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) { + if (cred.getType().equals(UserCredentialModel.PASSWORD)) { + return new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue()); + } + } + return false; } @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; + if (!validatePassword(user, password)) return false; + for (CredentialEntity cred : ((UserAdapter)user).getUser().getCredentials()) { + if (cred.getType().equals(UserCredentialModel.TOTP)) { + return new TimeBasedOTP().validate(token, cred.getValue().getBytes()); + } + } + return false; } @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); + CredentialEntity credentialEntity = null; + UserEntity userEntity = ((UserAdapter) user).getUser(); + for (CredentialEntity entity : userEntity.getCredentials()) { + if (entity.getType().equals(cred.getType())) { + credentialEntity = entity; + } } + + if (credentialEntity == null) { + credentialEntity = new CredentialEntity(); + credentialEntity.setType(cred.getType()); + credentialEntity.setDevice(cred.getDevice()); + userEntity.getCredentials().add(credentialEntity); + } + if (cred.getType().equals(UserCredentialModel.PASSWORD)) { + byte[] salt = Pbkdf2PasswordEncoder.getSalt(); + credentialEntity.setValue(new Pbkdf2PasswordEncoder(salt).encode(cred.getValue())); + credentialEntity.setSalt(salt); + } else { + credentialEntity.setValue(cred.getValue()); + } + credentialEntity.setDevice(cred.getDevice()); + + getMongoStore().updateEntity(userEntity, invocationContext); } @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); - } + DBObject query = new QueryBuilder() + .and("socialLinks.socialProvider").is(socialLink.getSocialProvider()) + .and("socialLinks.socialUsername").is(socialLink.getSocialUsername()) + .and("realmId").is(getId()) + .get(); + UserEntity userEntity = getMongoStore().loadSingleEntity(UserEntity.class, query, invocationContext); + return userEntity==null ? null : new UserAdapter(userEntity, invocationContext); } @Override public Set getSocialLinks(UserModel user) { - UserData userData = ((UserAdapter)user).getUser(); - String userId = userData.getId(); + UserEntity userEntity = ((UserAdapter)user).getUser(); + List linkEntities = userEntity.getSocialLinks(); - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("userId", userId) - .build(); - List dbSocialLinks = noSQL.loadObjects(SocialLinkData.class, query); + if (linkEntities == null) { + return Collections.EMPTY_SET; + } Set result = new HashSet(); - for (SocialLinkData socialLinkData : dbSocialLinks) { - SocialLinkModel model = new SocialLinkModel(socialLinkData.getSocialProvider(), socialLinkData.getSocialUsername()); + for (SocialLinkEntity socialLinkEntity : linkEntities) { + SocialLinkModel model = new SocialLinkModel(socialLinkEntity.getSocialProvider(), socialLinkEntity.getSocialUsername()); result.add(model); } return result; @@ -785,30 +827,26 @@ public class RealmAdapter implements RealmModel { @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()); + UserEntity userEntity = ((UserAdapter)user).getUser(); + SocialLinkEntity socialLinkEntity = new SocialLinkEntity(); + socialLinkEntity.setSocialProvider(socialLink.getSocialProvider()); + socialLinkEntity.setSocialUsername(socialLink.getSocialUsername()); - noSQL.saveObject(socialLinkData); + getMongoStore().pushItemToList(userEntity, "socialLinks", socialLinkEntity, true, invocationContext); } @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); + SocialLinkEntity socialLinkEntity = new SocialLinkEntity(); + socialLinkEntity.setSocialProvider(socialLink.getSocialProvider()); + socialLinkEntity.setSocialUsername(socialLink.getSocialUsername()); + + UserEntity userEntity = ((UserAdapter)user).getUser(); + getMongoStore().pullItemFromList(userEntity, "socialLinks", socialLinkEntity, invocationContext); } protected void updateRealm() { - noSQL.saveObject(realm); + getMongoStore().updateEntity(realm, invocationContext); } protected RequiredCredentialModel initRequiredCredentialModel(String type) { @@ -819,47 +857,109 @@ public class RealmAdapter implements RealmModel { return model; } + @Override + public List getUsers() { + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .get(); + List users = getMongoStore().loadEntities(UserEntity.class, query, invocationContext); + return convertUserEntities(users); + } + + @Override + public List searchForUser(String search) { + search = search.trim(); + Pattern caseInsensitivePattern = Pattern.compile("(?i:" + search + ")"); + + QueryBuilder nameBuilder; + int spaceInd = search.lastIndexOf(" "); + + // Case when we have search string like "ohn Bow". Then firstName must end with "ohn" AND lastName must start with "bow" (everything case-insensitive) + if (spaceInd != -1) { + String firstName = search.substring(0, spaceInd); + String lastName = search.substring(spaceInd + 1); + Pattern firstNamePattern = Pattern.compile("(?i:" + firstName + "$)"); + Pattern lastNamePattern = Pattern.compile("(?i:^" + lastName + ")"); + nameBuilder = new QueryBuilder().and( + new QueryBuilder().put("firstName").regex(firstNamePattern).get(), + new QueryBuilder().put("lastName").regex(lastNamePattern).get() + ); + } else { + // Case when we have search without spaces like "foo". The firstName OR lastName could be "foo" (everything case-insensitive) + nameBuilder = new QueryBuilder().or( + new QueryBuilder().put("firstName").regex(caseInsensitivePattern).get(), + new QueryBuilder().put("lastName").regex(caseInsensitivePattern).get() + ); + } + + QueryBuilder builder = new QueryBuilder().and( + new QueryBuilder().and("realmId").is(getId()).get(), + new QueryBuilder().or( + new QueryBuilder().put("loginName").regex(caseInsensitivePattern).get(), + new QueryBuilder().put("email").regex(caseInsensitivePattern).get(), + nameBuilder.get() + + ).get() + ); + + List users = getMongoStore().loadEntities(UserEntity.class, builder.get(), invocationContext); + return convertUserEntities(users); + } + @Override public List searchForUserByAttributes(Map attributes) { - NoSQLQueryBuilder queryBuilder = noSQL.createQueryBuilder(); + QueryBuilder queryBuilder = new QueryBuilder() + .and("realmId").is(getId()); + for (Map.Entry entry : attributes.entrySet()) { if (entry.getKey().equals(UserModel.LOGIN_NAME)) { - queryBuilder.andCondition("loginName", entry.getValue()); + queryBuilder.and("loginName").regex(Pattern.compile("(?i:" + entry.getValue() + "$)")); } else if (entry.getKey().equalsIgnoreCase(UserModel.FIRST_NAME)) { - queryBuilder.andCondition(UserModel.FIRST_NAME, entry.getValue()); + queryBuilder.and(UserModel.FIRST_NAME).regex(Pattern.compile("(?i:" + entry.getValue() + "$)")); } else if (entry.getKey().equalsIgnoreCase(UserModel.LAST_NAME)) { - queryBuilder.andCondition(UserModel.LAST_NAME, entry.getValue()); + queryBuilder.and(UserModel.LAST_NAME).regex(Pattern.compile("(?i:" + entry.getValue() + "$)")); } else if (entry.getKey().equalsIgnoreCase(UserModel.EMAIL)) { - queryBuilder.andCondition(UserModel.EMAIL, entry.getValue()); + queryBuilder.and(UserModel.EMAIL).regex(Pattern.compile("(?i:" + entry.getValue() + "$)")); } } - List users = noSQL.loadObjects(UserData.class, queryBuilder.build()); + List users = getMongoStore().loadEntities(UserEntity.class, queryBuilder.get(), invocationContext); + return convertUserEntities(users); + } + + protected List convertUserEntities(List userEntities) { List userModels = new ArrayList(); - for (UserData user : users) { - userModels.add(new UserAdapter(user, noSQL)); + for (UserEntity user : userEntities) { + userModels.add(new UserAdapter(user, invocationContext)); } return userModels; } @Override public Map getSmtpConfig() { - throw new RuntimeException("Not implemented"); + return realm.getSmtpConfig(); } @Override public void setSmtpConfig(Map smtpConfig) { - throw new RuntimeException("Not implemented"); + realm.setSmtpConfig(smtpConfig); + updateRealm(); } @Override public Map getSocialConfig() { - throw new RuntimeException("Not implemented"); + return realm.getSocialConfig(); } @Override public void setSocialConfig(Map socialConfig) { - throw new RuntimeException("Not implemented"); + realm.setSocialConfig(socialConfig); + updateRealm(); + } + + @Override + public AbstractMongoIdentifiableEntity getMongoEntity() { + return realm; } } 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 index 7b2692f42c..a3ed40b16a 100644 --- 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 @@ -1,22 +1,45 @@ package org.keycloak.models.mongo.keycloak.adapters; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; -import org.keycloak.models.mongo.api.NoSQL; -import org.keycloak.models.mongo.keycloak.data.RoleData; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.ApplicationEntity; +import org.keycloak.models.mongo.keycloak.entities.RealmEntity; +import org.keycloak.models.mongo.keycloak.entities.RoleEntity; +import org.keycloak.models.mongo.utils.MongoModelUtils; +import org.keycloak.models.utils.KeycloakModelUtils; /** * 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 { +public class RoleAdapter extends AbstractAdapter implements RoleModel { - private final RoleData role; - private final NoSQL noSQL; + private final RoleEntity role; + private RoleContainerModel roleContainer; - public RoleAdapter(RoleData roleData, NoSQL noSQL) { - this.role = roleData; - this.noSQL = noSQL; + public RoleAdapter(RoleEntity roleEntity, MongoStoreInvocationContext invContext) { + this(roleEntity, null, invContext); + } + + public RoleAdapter(RoleEntity roleEntity, RoleContainerModel roleContainer, MongoStoreInvocationContext invContext) { + super(invContext); + this.role = roleEntity; + this.roleContainer = roleContainer; + } + + @Override + public String getId() { + return role.getId(); } @Override @@ -24,6 +47,12 @@ public class RoleAdapter implements RoleModel { return role.getName(); } + @Override + public void setName(String name) { + role.setName(name); + updateRole(); + } + @Override public String getDescription() { return role.getDescription(); @@ -32,21 +61,84 @@ public class RoleAdapter implements RoleModel { @Override public void setDescription(String description) { role.setDescription(description); - noSQL.saveObject(role); + updateRole(); } @Override - public String getId() { - return role.getId(); + public boolean isComposite() { + return role.getCompositeRoleIds() != null && role.getCompositeRoleIds().size() > 0; + } + + protected void updateRole() { + getMongoStore().updateEntity(role, invocationContext); } @Override - public void setName(String name) { - role.setName(name); - noSQL.saveObject(role); + public void addCompositeRole(RoleModel childRole) { + getMongoStore().pushItemToList(role, "compositeRoleIds", childRole.getId(), true, invocationContext); } - public RoleData getRole() { + @Override + public void removeCompositeRole(RoleModel childRole) { + getMongoStore().pullItemFromList(role, "compositeRoleIds", childRole.getId(), invocationContext); + } + + @Override + public Set getComposites() { + if (role.getCompositeRoleIds() == null || role.getCompositeRoleIds().isEmpty()) { + return Collections.EMPTY_SET; + } + + DBObject query = new QueryBuilder() + .and("_id").in(MongoModelUtils.convertStringsToObjectIds(role.getCompositeRoleIds())) + .get(); + List childRoles = getMongoStore().loadEntities(RoleEntity.class, query, invocationContext); + + Set set = new HashSet(); + for (RoleEntity childRole : childRoles) { + set.add(new RoleAdapter(childRole, invocationContext)); + } + return set; + } + + @Override + public RoleContainerModel getContainer() { + if (roleContainer == null) { + // Compute it + if (role.getRealmId() != null) { + RealmEntity realm = getMongoStore().loadEntity(RealmEntity.class, role.getRealmId(), invocationContext); + if (realm == null) { + throw new IllegalStateException("Realm with id: " + role.getRealmId() + " doesn't exists"); + } + roleContainer = new RealmAdapter(realm, invocationContext); + } else if (role.getApplicationId() != null) { + ApplicationEntity appEntity = getMongoStore().loadEntity(ApplicationEntity.class, role.getApplicationId(), invocationContext); + if (appEntity == null) { + throw new IllegalStateException("Application with id: " + role.getApplicationId() + " doesn't exists"); + } + roleContainer = new ApplicationAdapter(appEntity, invocationContext); + } else { + throw new IllegalStateException("Both realmId and applicationId are null for role: " + this); + } + } + return roleContainer; + } + + @Override + public boolean hasRole(RoleModel role) { + if (this.equals(role)) return true; + if (!isComposite()) return false; + + Set visited = new HashSet(); + return KeycloakModelUtils.searchFor(role, this, visited); + } + + public RoleEntity getRole() { + return role; + } + + @Override + public AbstractMongoIdentifiableEntity getMongoEntity() { 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 index 3b208484bc..1a18387d25 100755 --- 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 @@ -1,10 +1,13 @@ package org.keycloak.models.mongo.keycloak.adapters; import org.keycloak.models.UserModel; -import org.keycloak.models.mongo.api.NoSQL; -import org.keycloak.models.mongo.keycloak.data.UserData; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.entities.UserEntity; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -15,14 +18,13 @@ import java.util.Set; * * @author Marek Posolda */ -public class UserAdapter implements UserModel { +public class UserAdapter extends AbstractAdapter implements UserModel { - private final UserData user; - private final NoSQL noSQL; + private final UserEntity user; - public UserAdapter(UserData userData, NoSQL noSQL) { - this.user = userData; - this.noSQL = noSQL; + public UserAdapter(UserEntity userEntity, MongoStoreInvocationContext invContext) { + super(invContext); + this.user = userEntity; } @Override @@ -38,7 +40,7 @@ public class UserAdapter implements UserModel { @Override public void setEnabled(boolean enabled) { user.setEnabled(enabled); - noSQL.saveObject(user); + updateUser(); } @Override @@ -49,7 +51,7 @@ public class UserAdapter implements UserModel { @Override public void setFirstName(String firstName) { user.setFirstName(firstName); - noSQL.saveObject(user); + updateUser(); } @Override @@ -60,7 +62,7 @@ public class UserAdapter implements UserModel { @Override public void setLastName(String lastName) { user.setLastName(lastName); - noSQL.saveObject(user); + updateUser(); } @Override @@ -71,7 +73,7 @@ public class UserAdapter implements UserModel { @Override public void setEmail(String email) { user.setEmail(email); - noSQL.saveObject(user); + updateUser(); } @Override @@ -82,61 +84,112 @@ public class UserAdapter implements UserModel { @Override public void setEmailVerified(boolean verified) { user.setEmailVerified(verified); - noSQL.saveObject(user); + updateUser(); } @Override public void setAttribute(String name, String value) { - user.setAttribute(name, value); + if (user.getAttributes() == null) { + user.setAttributes(new HashMap()); + } + + user.getAttributes().put(name, value); + updateUser(); } @Override public void removeAttribute(String name) { - user.removeAttribute(name); - noSQL.saveObject(user); + if (user.getAttributes() == null) return; + + user.getAttributes().remove(name); + updateUser(); } @Override public String getAttribute(String name) { - return user.getAttribute(name); + return user.getAttributes()==null ? null : user.getAttributes().get(name); } @Override public Map getAttributes() { - return user.getAttributes(); + return user.getAttributes()==null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(user.getAttributes()); } - public UserData getUser() { + public UserEntity 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); + public Set getWebOrigins() { + Set result = new HashSet(); + if (user.getWebOrigins() != null) { + result.addAll(user.getWebOrigins()); } + return result; + } + + @Override + public void setWebOrigins(Set webOrigins) { + List result = new ArrayList(); + result.addAll(webOrigins); + user.setWebOrigins(result); + updateUser(); + } + + @Override + public void addWebOrigin(String webOrigin) { + getMongoStore().pushItemToList(user, "webOrigins", webOrigin, true, invocationContext); + } + + @Override + public void removeWebOrigin(String webOrigin) { + getMongoStore().pullItemFromList(user, "webOrigins", webOrigin, invocationContext); + } + + @Override + public Set getRedirectUris() { + Set result = new HashSet(); + if (user.getRedirectUris() != null) { + result.addAll(user.getRedirectUris()); + } + return result; + } + + @Override + public void setRedirectUris(Set redirectUris) { + List result = new ArrayList(); + result.addAll(redirectUris); + user.setRedirectUris(result); + updateUser(); + } + + @Override + public void addRedirectUri(String redirectUri) { + getMongoStore().pushItemToList(user, "redirectUris", redirectUri, true, invocationContext); + } + + @Override + public void removeRedirectUri(String redirectUri) { + getMongoStore().pullItemFromList(user, "redirectUris", redirectUri, invocationContext); + } + + @Override + public Set getRequiredActions() { + Set result = new HashSet(); + if (user.getRequiredActions() != null) { + result.addAll(user.getRequiredActions()); + } + return result; } @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); - } + getMongoStore().pushItemToList(user, "requiredActions", action, true, invocationContext); } @Override public void removeRequiredAction(RequiredAction action) { - noSQL.pullItemFromList(user, "requiredActions", action); + getMongoStore().pullItemFromList(user, "requiredActions", action, invocationContext); } @Override @@ -147,46 +200,15 @@ public class UserAdapter implements UserModel { @Override public void setTotp(boolean totp) { user.setTotp(totp); - noSQL.saveObject(user); + updateUser(); + } + + protected void updateUser() { + getMongoStore().updateEntity(user, invocationContext); } @Override - public Set getWebOrigins() { - return null; //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void setWebOrigins(Set webOrigins) { - //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void addWebOrigin(String webOrigin) { - //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void removeWebOrigin(String webOrigin) { - //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public Set getRedirectUris() { - return null; //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void setRedirectUris(Set redirectUris) { - //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void addRedirectUri(String redirectUri) { - //To change body of implemented methods use File | Settings | File Templates. - } - - @Override - public void removeRedirectUri(String redirectUri) { - //To change body of implemented methods use File | Settings | File Templates. + public AbstractMongoIdentifiableEntity getMongoEntity() { + return 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 deleted file mode 100755 index b6a3eb1171..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/PasswordCredentialHandler.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.keycloak.models.mongo.keycloak.credentials; - -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; - -import java.util.Date; -import java.util.Map; -import java.util.UUID; - -/** - * 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 deleted file mode 100755 index 45a76f757c..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/credentials/TOTPCredentialHandler.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.keycloak.models.mongo.keycloak.credentials; - -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 java.util.Date; -import java.util.Map; - -import static org.picketlink.common.util.StringUtil.isNullOrEmpty; -import static org.picketlink.idm.credential.util.TimeBasedOTP.*; - -/** - * 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/OAuthClientData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java deleted file mode 100644 index 67f74ee6a3..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/OAuthClientData.java +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100755 index d9aa0ae59d..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RealmData.java +++ /dev/null @@ -1,219 +0,0 @@ -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; - -import java.util.List; - -/** - * @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 deleted file mode 100644 index e46ee9fd07..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RequiredCredentialData.java +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100755 index 9bd14d23f1..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/RoleData.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.keycloak.models.mongo.keycloak.data; - -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 java.util.List; - -/** - * @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 deleted file mode 100644 index 37ea43d44e..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/SocialLinkData.java +++ /dev/null @@ -1,55 +0,0 @@ -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/credentials/OTPData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java deleted file mode 100755 index 6983f83c3a..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/OTPData.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.keycloak.models.mongo.keycloak.data.credentials; - -import org.keycloak.models.mongo.api.AbstractNoSQLObject; -import org.keycloak.models.mongo.api.NoSQLCollection; -import org.keycloak.models.mongo.api.NoSQLField; - -import java.util.Date; - -/** - * @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 deleted file mode 100755 index 6ac585f58c..0000000000 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/credentials/PasswordData.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.keycloak.models.mongo.keycloak.data.credentials; - -import org.keycloak.models.mongo.api.AbstractNoSQLObject; -import org.keycloak.models.mongo.api.NoSQLCollection; -import org.keycloak.models.mongo.api.NoSQLField; - -import java.util.Date; - -/** - * @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/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/ApplicationEntity.java similarity index 54% rename from model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/ApplicationData.java rename to model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/ApplicationEntity.java index 5ceb788dbb..39a62346a1 100644 --- 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/entities/ApplicationEntity.java @@ -1,19 +1,22 @@ -package org.keycloak.models.mongo.keycloak.data; +package org.keycloak.models.mongo.keycloak.entities; -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 java.util.ArrayList; +import java.util.List; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoCollection; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; /** * @author Marek Posolda */ -@NoSQLCollection(collectionName = "applications") -public class ApplicationData implements NoSQLObject { +@MongoCollection(collectionName = "applications") +public class ApplicationEntity extends AbstractMongoIdentifiableEntity implements MongoEntity { - private String id; private String name; private boolean enabled; private boolean surrogateAuthRequired; @@ -23,16 +26,10 @@ public class ApplicationData implements NoSQLObject { private String resourceUserId; private String realmId; - @NoSQLId - public String getId() { - return id; - } + // We are using names of defaultRoles (not ids) + private List defaultRoles = new ArrayList(); - public void setId(String id) { - this.id = id; - } - - @NoSQLField + @MongoField public String getName() { return name; } @@ -41,7 +38,7 @@ public class ApplicationData implements NoSQLObject { this.name = name; } - @NoSQLField + @MongoField public boolean isEnabled() { return enabled; } @@ -50,7 +47,7 @@ public class ApplicationData implements NoSQLObject { this.enabled = enabled; } - @NoSQLField + @MongoField public boolean isSurrogateAuthRequired() { return surrogateAuthRequired; } @@ -59,7 +56,7 @@ public class ApplicationData implements NoSQLObject { this.surrogateAuthRequired = surrogateAuthRequired; } - @NoSQLField + @MongoField public String getManagementUrl() { return managementUrl; } @@ -68,7 +65,7 @@ public class ApplicationData implements NoSQLObject { this.managementUrl = managementUrl; } - @NoSQLField + @MongoField public String getBaseUrl() { return baseUrl; } @@ -77,7 +74,7 @@ public class ApplicationData implements NoSQLObject { this.baseUrl = baseUrl; } - @NoSQLField + @MongoField public String getResourceUserId() { return resourceUserId; } @@ -86,7 +83,7 @@ public class ApplicationData implements NoSQLObject { this.resourceUserId = resourceUserId; } - @NoSQLField + @MongoField public String getRealmId() { return realmId; } @@ -95,15 +92,24 @@ public class ApplicationData implements NoSQLObject { this.realmId = realmId; } + @MongoField + public List getDefaultRoles() { + return defaultRoles; + } + + public void setDefaultRoles(List defaultRoles) { + this.defaultRoles = defaultRoles; + } + @Override - public void afterRemove(NoSQL noSQL) { + public void afterRemove(MongoStoreInvocationContext context) { // Remove resourceUser of this application - noSQL.removeObject(UserData.class, resourceUserId); + context.getMongoStore().removeEntity(UserEntity.class, resourceUserId, context); // Remove all roles, which belongs to this application - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("applicationId", id) - .build(); - noSQL.removeObjects(RoleData.class, query); + DBObject query = new QueryBuilder() + .and("applicationId").is(getId()) + .get(); + context.getMongoStore().removeEntities(RoleEntity.class, query, context); } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/CredentialEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/CredentialEntity.java new file mode 100644 index 0000000000..6ab322da22 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/CredentialEntity.java @@ -0,0 +1,51 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; + +/** + * @author Marek Posolda + */ +public class CredentialEntity implements MongoEntity { + + private String type; + private String value; + private String device; + private byte[] salt; + + @MongoField + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @MongoField + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @MongoField + public String getDevice() { + return device; + } + + public void setDevice(String device) { + this.device = device; + } + + @MongoField + public byte[] getSalt() { + return salt; + } + + public void setSalt(byte[] salt) { + this.salt = salt; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/OAuthClientEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/OAuthClientEntity.java new file mode 100644 index 0000000000..f2875b1be0 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/OAuthClientEntity.java @@ -0,0 +1,52 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoCollection; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +/** + * @author Marek Posolda + */ +@MongoCollection(collectionName = "oauthClients") +public class OAuthClientEntity extends AbstractMongoIdentifiableEntity implements MongoEntity { + + private String name; + + private String oauthAgentId; + private String realmId; + + @MongoField + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @MongoField + public String getOauthAgentId() { + return oauthAgentId; + } + + public void setOauthAgentId(String oauthUserId) { + this.oauthAgentId = oauthUserId; + } + + @MongoField + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + @Override + public void afterRemove(MongoStoreInvocationContext context) { + // Remove user of this oauthClient + context.getMongoStore().removeEntity(UserEntity.class, oauthAgentId, context); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java new file mode 100755 index 0000000000..6ee2fdf652 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RealmEntity.java @@ -0,0 +1,265 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoCollection; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Marek Posolda + */ +@MongoCollection(collectionName = "realms") +public class RealmEntity extends AbstractMongoIdentifiableEntity implements MongoEntity { + + private String name; + private boolean enabled; + private boolean sslNotRequired; + private boolean registrationAllowed; + private boolean verifyEmail; + private boolean resetPasswordAllowed; + private boolean social; + private boolean updateProfileOnInitialSocialLogin; + private String passwordPolicy; + + private int tokenLifespan; + private int accessCodeLifespan; + private int accessCodeLifespanUserAction; + + private String publicKeyPem; + private String privateKeyPem; + + private String loginTheme; + private String accountTheme; + + // We are using names of defaultRoles (not ids) + private List defaultRoles = new ArrayList(); + + private List requiredCredentials = new ArrayList(); + private List requiredApplicationCredentials = new ArrayList(); + private List requiredOAuthClientCredentials = new ArrayList(); + + private Map smtpConfig = new HashMap(); + private Map socialConfig = new HashMap(); + + @MongoField + public String getName() { + return name; + } + + public void setName(String realmName) { + this.name = realmName; + } + + @MongoField + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @MongoField + public boolean isSslNotRequired() { + return sslNotRequired; + } + + public void setSslNotRequired(boolean sslNotRequired) { + this.sslNotRequired = sslNotRequired; + } + + @MongoField + public boolean isRegistrationAllowed() { + return registrationAllowed; + } + + public void setRegistrationAllowed(boolean registrationAllowed) { + this.registrationAllowed = registrationAllowed; + } + + @MongoField + public boolean isVerifyEmail() { + return verifyEmail; + } + + public void setVerifyEmail(boolean verifyEmail) { + this.verifyEmail = verifyEmail; + } + + @MongoField + public boolean isResetPasswordAllowed() { + return resetPasswordAllowed; + } + + public void setResetPasswordAllowed(boolean resetPasswordAllowed) { + this.resetPasswordAllowed = resetPasswordAllowed; + } + + @MongoField + public boolean isSocial() { + return social; + } + + public void setSocial(boolean social) { + this.social = social; + } + + @MongoField + public boolean isUpdateProfileOnInitialSocialLogin() { + return updateProfileOnInitialSocialLogin; + } + + public void setUpdateProfileOnInitialSocialLogin(boolean updateProfileOnInitialSocialLogin) { + this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin; + } + + @MongoField + public String getPasswordPolicy() { + return passwordPolicy; + } + + public void setPasswordPolicy(String passwordPolicy) { + this.passwordPolicy = passwordPolicy; + } + + @MongoField + public int getTokenLifespan() { + return tokenLifespan; + } + + public void setTokenLifespan(int tokenLifespan) { + this.tokenLifespan = tokenLifespan; + } + + @MongoField + public int getAccessCodeLifespan() { + return accessCodeLifespan; + } + + public void setAccessCodeLifespan(int accessCodeLifespan) { + this.accessCodeLifespan = accessCodeLifespan; + } + + @MongoField + public int getAccessCodeLifespanUserAction() { + return accessCodeLifespanUserAction; + } + + public void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction) { + this.accessCodeLifespanUserAction = accessCodeLifespanUserAction; + } + + @MongoField + public String getPublicKeyPem() { + return publicKeyPem; + } + + public void setPublicKeyPem(String publicKeyPem) { + this.publicKeyPem = publicKeyPem; + } + + @MongoField + public String getPrivateKeyPem() { + return privateKeyPem; + } + + public void setPrivateKeyPem(String privateKeyPem) { + this.privateKeyPem = privateKeyPem; + } + + @MongoField + public String getLoginTheme() { + return loginTheme; + } + + public void setLoginTheme(String loginTheme) { + this.loginTheme = loginTheme; + } + + @MongoField + public String getAccountTheme() { + return accountTheme; + } + + public void setAccountTheme(String accountTheme) { + this.accountTheme = accountTheme; + } + + @MongoField + public List getDefaultRoles() { + return defaultRoles; + } + + public void setDefaultRoles(List defaultRoles) { + this.defaultRoles = defaultRoles; + } + + @MongoField + public List getRequiredCredentials() { + return requiredCredentials; + } + + public void setRequiredCredentials(List requiredCredentials) { + this.requiredCredentials = requiredCredentials; + } + + @MongoField + public List getRequiredApplicationCredentials() { + return requiredApplicationCredentials; + } + + public void setRequiredApplicationCredentials(List requiredApplicationCredentials) { + this.requiredApplicationCredentials = requiredApplicationCredentials; + } + + @MongoField + public List getRequiredOAuthClientCredentials() { + return requiredOAuthClientCredentials; + } + + public void setRequiredOAuthClientCredentials(List requiredOAuthClientCredentials) { + this.requiredOAuthClientCredentials = requiredOAuthClientCredentials; + } + + @MongoField + public Map getSmtpConfig() { + return smtpConfig; + } + + public void setSmtpConfig(Map smptConfig) { + this.smtpConfig = smptConfig; + } + + @MongoField + public Map getSocialConfig() { + return socialConfig; + } + + public void setSocialConfig(Map socialConfig) { + this.socialConfig = socialConfig; + } + + @Override + public void afterRemove(MongoStoreInvocationContext context) { + DBObject query = new QueryBuilder() + .and("realmId").is(getId()) + .get(); + + // Remove all users of this realm + context.getMongoStore().removeEntities(UserEntity.class, query, context); + + // Remove all roles of this realm + context.getMongoStore().removeEntities(RoleEntity.class, query, context); + + // Remove all applications of this realm + context.getMongoStore().removeEntities(ApplicationEntity.class, query, context); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RequiredCredentialEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RequiredCredentialEntity.java new file mode 100644 index 0000000000..f39e327f43 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RequiredCredentialEntity.java @@ -0,0 +1,51 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; + +/** + * @author Marek Posolda + */ +public class RequiredCredentialEntity implements MongoEntity { + + private String type; + private boolean input; + private boolean secret; + private String formLabel; + + @MongoField + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @MongoField + public boolean isInput() { + return input; + } + + public void setInput(boolean input) { + this.input = input; + } + + @MongoField + public boolean isSecret() { + return secret; + } + + public void setSecret(boolean secret) { + this.secret = secret; + } + + @MongoField + public String getFormLabel() { + return formLabel; + } + + public void setFormLabel(String formLabel) { + this.formLabel = formLabel; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RoleEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RoleEntity.java new file mode 100755 index 0000000000..e2e97279dc --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/RoleEntity.java @@ -0,0 +1,131 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.jboss.logging.Logger; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoCollection; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; +import org.keycloak.models.mongo.api.MongoStore; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; + +import java.util.List; + +/** + * @author Marek Posolda + */ +@MongoCollection(collectionName = "roles") +public class RoleEntity extends AbstractMongoIdentifiableEntity implements MongoEntity { + + private static final Logger logger = Logger.getLogger(RoleEntity.class); + + private String name; + private String description; + + private List compositeRoleIds; + + private String realmId; + private String applicationId; + + @MongoField + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @MongoField + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @MongoField + public List getCompositeRoleIds() { + return compositeRoleIds; + } + + public void setCompositeRoleIds(List compositeRoleIds) { + this.compositeRoleIds = compositeRoleIds; + } + + @MongoField + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } + + @MongoField + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + @Override + public void afterRemove(MongoStoreInvocationContext invContext) { + MongoStore mongoStore = invContext.getMongoStore(); + + // Remove this role from all users, which has it + DBObject query = new QueryBuilder() + .and("roleIds").is(getId()) + .get(); + + List users = mongoStore.loadEntities(UserEntity.class, query, invContext); + for (UserEntity user : users) { + logger.info("Removing role " + getName() + " from user " + user.getLoginName()); + mongoStore.pullItemFromList(user, "roleIds", getId(), invContext); + } + + // Remove this scope from all users, which has it + query = new QueryBuilder() + .and("scopeIds").is(getId()) + .get(); + + users = mongoStore.loadEntities(UserEntity.class, query, invContext); + for (UserEntity user : users) { + logger.info("Removing scope " + getName() + " from user " + user.getLoginName()); + mongoStore.pullItemFromList(user, "scopeIds", getId(), invContext); + } + + // Remove defaultRoles from realm + if (realmId != null) { + RealmEntity realmEntity = mongoStore.loadEntity(RealmEntity.class, realmId, invContext); + + // Realm might be already removed at this point + if (realmEntity != null) { + mongoStore.pullItemFromList(realmEntity, "defaultRoles", getId(), invContext); + } + } + + // Remove defaultRoles from application + if (applicationId != null) { + ApplicationEntity appEntity = mongoStore.loadEntity(ApplicationEntity.class, applicationId, invContext); + + // Application might be already removed at this point + if (appEntity != null) { + mongoStore.pullItemFromList(appEntity, "defaultRoles", getId(), invContext); + } + } + + // Remove this role from others who has it as composite + query = new QueryBuilder() + .and("compositeRoleIds").is(getId()) + .get(); + List parentRoles = mongoStore.loadEntities(RoleEntity.class, query, invContext); + for (RoleEntity role : parentRoles) { + mongoStore.pullItemFromList(role, "compositeRoleIds", getId(), invContext); + } + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/SocialLinkEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/SocialLinkEntity.java new file mode 100644 index 0000000000..85ae5c0b7b --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/SocialLinkEntity.java @@ -0,0 +1,58 @@ +package org.keycloak.models.mongo.keycloak.entities; + +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; + +/** + * @author Marek Posolda + */ +public class SocialLinkEntity implements MongoEntity { + + private String socialUsername; + private String socialProvider; + + @MongoField + public String getSocialUsername() { + return socialUsername; + } + + public void setSocialUsername(String socialUsername) { + this.socialUsername = socialUsername; + } + + @MongoField + public String getSocialProvider() { + return socialProvider; + } + + public void setSocialProvider(String socialProvider) { + this.socialProvider = socialProvider; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SocialLinkEntity that = (SocialLinkEntity) o; + + if (socialProvider != null && (that.socialProvider == null || !socialProvider.equals(that.socialProvider))) return false; + if (socialUsername != null && (that.socialUsername == null || !socialUsername.equals(that.socialUsername))) return false; + if (socialProvider == null && that.socialProvider != null)return false; + if (socialUsername == null && that.socialUsername != null) return false; + + return true; + } + + @Override + public int hashCode() { + int code = 1; + if (socialUsername != null) { + code = code * 13; + } + if (socialProvider != null) { + code = code * 17; + } + return code; + } +} 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/entities/UserEntity.java similarity index 54% rename from model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/data/UserData.java rename to model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/UserEntity.java index c57bca38e9..fdd69a2141 100755 --- 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/entities/UserEntity.java @@ -1,26 +1,21 @@ -package org.keycloak.models.mongo.keycloak.data; +package org.keycloak.models.mongo.keycloak.entities; -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; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoCollection; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; +import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * @author Marek Posolda */ -@NoSQLCollection(collectionName = "users") -public class UserData extends AbstractAttributedNoSQLObject { +@MongoCollection(collectionName = "users") +public class UserEntity extends AbstractMongoIdentifiableEntity implements MongoEntity { - private static final Logger logger = Logger.getLogger(UserData.class); - - private String id; private String loginName; private String firstName; private String lastName; @@ -33,18 +28,15 @@ public class UserData extends AbstractAttributedNoSQLObject { private List roleIds; private List scopeIds; + + private Map attributes; + private List webOrigins; + private List redirectUris; private List requiredActions; + private List credentials = new ArrayList(); + private List socialLinks; - @NoSQLId - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - @NoSQLField + @MongoField public String getLoginName() { return loginName; } @@ -53,7 +45,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.loginName = loginName; } - @NoSQLField + @MongoField public String getFirstName() { return firstName; } @@ -62,7 +54,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.firstName = firstName; } - @NoSQLField + @MongoField public String getLastName() { return lastName; } @@ -71,7 +63,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.lastName = lastName; } - @NoSQLField + @MongoField public String getEmail() { return email; } @@ -80,7 +72,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.email = email; } - @NoSQLField + @MongoField public boolean isEmailVerified() { return emailVerified; } @@ -89,7 +81,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.emailVerified = emailVerified; } - @NoSQLField + @MongoField public boolean isEnabled() { return enabled; } @@ -98,7 +90,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.enabled = enabled; } - @NoSQLField + @MongoField public boolean isTotp() { return totp; } @@ -107,7 +99,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.totp = totp; } - @NoSQLField + @MongoField public String getRealmId() { return realmId; } @@ -116,7 +108,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.realmId = realmId; } - @NoSQLField + @MongoField public List getRoleIds() { return roleIds; } @@ -125,7 +117,7 @@ public class UserData extends AbstractAttributedNoSQLObject { this.roleIds = roleIds; } - @NoSQLField + @MongoField public List getScopeIds() { return scopeIds; } @@ -134,7 +126,34 @@ public class UserData extends AbstractAttributedNoSQLObject { this.scopeIds = scopeIds; } - @NoSQLField + @MongoField + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + @MongoField + public List getWebOrigins() { + return webOrigins; + } + + public void setWebOrigins(List webOrigins) { + this.webOrigins = webOrigins; + } + + @MongoField + public List getRedirectUris() { + return redirectUris; + } + + public void setRedirectUris(List redirectUris) { + this.redirectUris = redirectUris; + } + + @MongoField public List getRequiredActions() { return requiredActions; } @@ -143,25 +162,21 @@ public class UserData extends AbstractAttributedNoSQLObject { this.requiredActions = requiredActions; } - @Override - public void afterRemove(NoSQL noSQL) { - NoSQLQuery query = noSQL.createQueryBuilder() - .andCondition("userId", id) - .build(); + @MongoField + public List getCredentials() { + return credentials; + } - // Remove social links and passwords of this user - noSQL.removeObjects(SocialLinkData.class, query); - noSQL.removeObjects(PasswordData.class, query); + public void setCredentials(List credentials) { + this.credentials = credentials; + } - // Remove this user from all realms, which have him as an admin - NoSQLQuery realmQuery = noSQL.createQueryBuilder() - .andCondition("realmAdmins", id) - .build(); + @MongoField + public List getSocialLinks() { + return socialLinks; + } - 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()); - } + public void setSocialLinks(List socialLinks) { + this.socialLinks = socialLinks; } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoConfiguration.java b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoConfiguration.java new file mode 100644 index 0000000000..7ba22d18f1 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoConfiguration.java @@ -0,0 +1,44 @@ +package org.keycloak.models.mongo.utils; + +/** + * Encapsulates all info about configuration of MongoDB instance + * + * @author Marek Posolda + */ +public class MongoConfiguration { + + private final String host; + private final int port; + private final String dbName; + + private final boolean clearCollectionsOnStartup; + + public MongoConfiguration(String host, int port, String dbName, boolean clearCollectionsOnStartup) { + this.host = host; + this.port = port; + this.dbName = dbName; + this.clearCollectionsOnStartup = clearCollectionsOnStartup; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public String getDbName() { + return dbName; + } + + public boolean isClearCollectionsOnStartup() { + return clearCollectionsOnStartup; + } + + @Override + public String toString() { + return String.format("MongoConfiguration: host: %s, port: %d, dbName: %s, clearCollectionsOnStartup: %b", + host, port, dbName, clearCollectionsOnStartup); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java new file mode 100644 index 0000000000..4be29ecb34 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/MongoModelUtils.java @@ -0,0 +1,59 @@ +package org.keycloak.models.mongo.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.bson.types.ObjectId; +import org.keycloak.models.UserModel; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.adapters.UserAdapter; +import org.keycloak.models.mongo.keycloak.entities.RoleEntity; +import org.keycloak.models.mongo.keycloak.entities.UserEntity; + +/** + * @author Marek Posolda + */ +public class MongoModelUtils { + + public static List convertStringsToObjectIds(Collection strings) { + List result = new ArrayList(); + for (String id : strings) { + result.add(new ObjectId(id)); + } + return result; + } + + // Get everything including both application and realm roles + public static List getAllRolesOfUser(UserModel user, MongoStoreInvocationContext invContext) { + UserEntity userEntity = ((UserAdapter)user).getUser(); + List roleIds = userEntity.getRoleIds(); + + if (roleIds == null || roleIds.isEmpty()) { + return Collections.EMPTY_LIST; + } + + DBObject query = new QueryBuilder() + .and("_id").in(convertStringsToObjectIds(roleIds)) + .get(); + return invContext.getMongoStore().loadEntities(RoleEntity.class, query, invContext); + } + + // Get everything including both application and realm scopes + public static List getAllScopesOfUser(UserModel user, MongoStoreInvocationContext invContext) { + UserEntity userEntity = ((UserAdapter)user).getUser(); + List scopeIds = userEntity.getScopeIds(); + + if (scopeIds == null || scopeIds.isEmpty()) { + return Collections.EMPTY_LIST; + } + + DBObject query = new QueryBuilder() + .and("_id").in(convertStringsToObjectIds(scopeIds)) + .get(); + return invContext.getMongoStore().loadEntities(RoleEntity.class, query, invContext); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/utils/SystemPropertiesConfigurationProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/SystemPropertiesConfigurationProvider.java new file mode 100755 index 0000000000..ce24c63795 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/utils/SystemPropertiesConfigurationProvider.java @@ -0,0 +1,55 @@ +package org.keycloak.models.mongo.utils; + +/** + * @author Marek Posolda + */ +public class SystemPropertiesConfigurationProvider { + + private static final String MONGO_HOST = "keycloak.mongo.host"; + private static final String MONGO_PORT = "keycloak.mongo.port"; + private static final String MONGO_DB_NAME = "keycloak.mongo.db"; + private static final String MONGO_CLEAR_ON_STARTUP = "keycloak.mongo.clearOnStartup"; + + // Property names from Liveoak . Those are used as fallback in case that original value is not available + private static final String MONGO_HOST_2 = "mongo.host"; + private static final String MONGO_PORT_2 = "mongo.port"; + private static final String MONGO_DB_NAME_2 = "mongo.db"; + private static final String MONGO_CLEAR_ON_STARTUP_2 = "mongo.clearCollectionsOnStartup"; + + // Port where MongoDB instance is normally started on linux. This port should be used if we're not starting embedded instance + private static final String MONGO_DEFAULT_PORT = "27017"; + + public static String getMongoHost() { + return getSystemPropertyWithFallback(MONGO_HOST, MONGO_HOST_2, "localhost"); + } + + public static int getMongoPort() { + String portProp = getSystemPropertyWithFallback(MONGO_PORT, MONGO_PORT_2, MONGO_DEFAULT_PORT); + return Integer.parseInt(portProp); + } + + public static String getMongoDbName() { + return getSystemPropertyWithFallback(MONGO_DB_NAME, MONGO_DB_NAME_2, "keycloak"); + } + + public static boolean isClearCollectionsOnStartup() { + String property = getSystemPropertyWithFallback(MONGO_CLEAR_ON_STARTUP, MONGO_CLEAR_ON_STARTUP_2, "false"); + return "true".equalsIgnoreCase(property); + } + + // Check if property propName1 (like "keycloak.mongo.host" is available and if not, then fallback to property "mongo.host" ) + private static String getSystemPropertyWithFallback(String propName1, String propName2, String defaultValue) { + String propValue1 = System.getProperty(propName1); + return propValue1!=null ? propValue1 : System.getProperty(propName2, defaultValue); + } + + // Create configuration based on system properties + public static MongoConfiguration createConfiguration() { + return new MongoConfiguration( + getMongoHost(), + getMongoPort(), + getMongoDbName(), + isClearCollectionsOnStartup() + ); + } +} 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 index 386ca31127..81bd7d8dce 100755 --- 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 @@ -1,20 +1,20 @@ package org.keycloak.models.mongo.test; -import org.keycloak.models.mongo.api.AbstractNoSQLObject; -import org.keycloak.models.mongo.api.NoSQLField; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoField; import java.util.List; /** * @author Marek Posolda */ -public class Address extends AbstractNoSQLObject { +public class Address implements MongoEntity { private String street; private int number; private List flatNumbers; - @NoSQLField + @MongoField public String getStreet() { return street; } @@ -23,7 +23,7 @@ public class Address extends AbstractNoSQLObject { this.street = street; } - @NoSQLField + @MongoField public int getNumber() { return number; } @@ -32,7 +32,7 @@ public class Address extends AbstractNoSQLObject { this.number = number; } - @NoSQLField + @MongoField public List getFlatNumbers() { return 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 index 3b8651a254..7ee814131c 100755 --- 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 @@ -1,14 +1,19 @@ package org.keycloak.models.mongo.test; import com.mongodb.DB; +import com.mongodb.DBObject; import com.mongodb.MongoClient; +import com.mongodb.QueryBuilder; 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; +import org.junit.Test; +import org.keycloak.models.mongo.api.MongoEntity; +import org.keycloak.models.mongo.api.MongoStore; +import org.keycloak.models.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.impl.MongoStoreImpl; +import org.keycloak.models.mongo.impl.context.TransactionMongoStoreInvocationContext; +import org.keycloak.models.mongo.utils.SystemPropertiesConfigurationProvider; import java.net.UnknownHostException; import java.util.ArrayList; @@ -20,22 +25,22 @@ import java.util.List; */ public class MongoDBModelTest { - private static final Class[] MANAGED_DATA_TYPES = (Class[])new Class[] { + private static final Class[] MANAGED_DATA_TYPES = (Class[])new Class[] { Person.class, Address.class, }; private MongoClient mongoClient; - private NoSQL mongoDB; + private MongoStore mongoStore; @Before public void before() throws Exception { try { // TODO: authentication support - mongoClient = new MongoClient("localhost", 27017); + mongoClient = new MongoClient("localhost", SystemPropertiesConfigurationProvider.getMongoPort()); DB db = mongoClient.getDB("keycloakTest"); - mongoDB = new MongoDBImpl(db, true, MANAGED_DATA_TYPES); + mongoStore = new MongoStoreImpl(db, true, MANAGED_DATA_TYPES); } catch (UnknownHostException e) { throw new RuntimeException(e); @@ -47,25 +52,27 @@ public class MongoDBModelTest { mongoClient.close(); } - // @Test + @Test public void mongoModelTest() throws Exception { + MongoStoreInvocationContext context = new TransactionMongoStoreInvocationContext(mongoStore); + // Add some user Person john = new Person(); john.setFirstName("john"); john.setAge(25); john.setGender(Person.Gender.MALE); - mongoDB.saveObject(john); + mongoStore.insertEntity(john, context); // Add another user Person mary = new Person(); mary.setFirstName("mary"); - mary.setKids(Arrays.asList(new String[] {"Peter", "Paul", "Wendy"})); + mary.setKids(asList("Peter", "Paul", "Wendy")); Address addr1 = new Address(); addr1.setStreet("Elm"); addr1.setNumber(5); - addr1.setFlatNumbers(Arrays.asList(new String[] {"flat1", "flat2"})); + addr1.setFlatNumbers(asList("flat1", "flat2")); Address addr2 = new Address(); List
addresses = new ArrayList
(); addresses.add(addr1); @@ -74,13 +81,14 @@ public class MongoDBModelTest { mary.setAddresses(addresses); mary.setMainAddress(addr1); mary.setGender(Person.Gender.FEMALE); - mary.setGenders(Arrays.asList(new Person.Gender[] {Person.Gender.FEMALE})); - mongoDB.saveObject(mary); + mary.setGenders(asList(Person.Gender.FEMALE)); - Assert.assertEquals(2, mongoDB.loadObjects(Person.class, mongoDB.createQueryBuilder().build()).size()); + mongoStore.insertEntity(mary, context); - NoSQLQuery query = mongoDB.createQueryBuilder().andCondition("addresses.flatNumbers", "flat1").build(); - List persons = mongoDB.loadObjects(Person.class, query); + Assert.assertEquals(2, mongoStore.loadEntities(Person.class, new QueryBuilder().get(), context).size()); + + DBObject query = new QueryBuilder().and("addresses.flatNumbers").is("flat1").get(); + List persons = mongoStore.loadEntities(Person.class, query, context); Assert.assertEquals(1, persons.size()); mary = persons.get(0); Assert.assertEquals(mary.getFirstName(), "mary"); @@ -89,15 +97,15 @@ public class MongoDBModelTest { Assert.assertEquals(Address.class, mary.getAddresses().get(0).getClass()); // Test push/pull - mongoDB.pushItemToList(mary, "kids", "Pauline"); - mongoDB.pullItemFromList(mary, "kids", "Paul"); + mongoStore.pushItemToList(mary, "kids", "Pauline", true, context); + mongoStore.pullItemFromList(mary, "kids", "Paul", context); Address addr3 = new Address(); addr3.setNumber(6); addr3.setStreet("Broadway"); - mongoDB.pushItemToList(mary, "addresses", addr3); + mongoStore.pushItemToList(mary, "addresses", addr3, true, context); - mary = mongoDB.loadObject(Person.class, mary.getId()); + mary = mongoStore.loadEntity(Person.class, mary.getId(), context); Assert.assertEquals(3, mary.getKids().size()); Assert.assertTrue(mary.getKids().contains("Pauline")); Assert.assertFalse(mary.getKids().contains("Paul")); @@ -107,5 +115,32 @@ public class MongoDBModelTest { Assert.assertEquals(5, mainAddress.getNumber()); Assert.assertEquals(Person.Gender.FEMALE, mary.getGender()); Assert.assertTrue(mary.getGenders().contains(Person.Gender.FEMALE)); + + + // Some test of Map (attributes) + mary.addAttribute("attr1", "value1"); + mary.addAttribute("attr2", "value2"); + mary.addAttribute("attr.some3", "value3"); + mongoStore.updateEntity(mary, context); + + mary = mongoStore.loadEntity(Person.class, mary.getId(), context); + Assert.assertEquals(3, mary.getAttributes().size()); + + mary.removeAttribute("attr2"); + mary.removeAttribute("nonExisting"); + mongoStore.updateEntity(mary, context); + + mary = mongoStore.loadEntity(Person.class, mary.getId(), context); + Assert.assertEquals(2, mary.getAttributes().size()); + Assert.assertEquals("value1", mary.getAttributes().get("attr1")); + Assert.assertEquals("value3", mary.getAttributes().get("attr.some3")); + + context.commit(); + } + + private List asList(T... objects) { + List list = new ArrayList(); + list.addAll(Arrays.asList(objects)); + return list; } } 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 index 7f8d0d9c24..47ac69bdd9 100755 --- 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 @@ -1,19 +1,19 @@ package org.keycloak.models.mongo.test; -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; +import org.keycloak.models.mongo.api.AbstractMongoIdentifiableEntity; +import org.keycloak.models.mongo.api.MongoCollection; +import org.keycloak.models.mongo.api.MongoField; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @author Marek Posolda */ -@NoSQLCollection(collectionName = "persons") -public class Person extends AbstractNoSQLObject { +@MongoCollection(collectionName = "persons") +public class Person extends AbstractMongoIdentifiableEntity { - private String id; private String firstName; private int age; private List kids; @@ -21,18 +21,9 @@ public class Person extends AbstractNoSQLObject { private Address mainAddress; private Gender gender; private List genders; + private Map attributes = new HashMap(); - - @NoSQLId - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - @NoSQLField + @MongoField public String getFirstName() { return firstName; } @@ -41,7 +32,7 @@ public class Person extends AbstractNoSQLObject { this.firstName = firstName; } - @NoSQLField + @MongoField public int getAge() { return age; } @@ -50,7 +41,7 @@ public class Person extends AbstractNoSQLObject { this.age = age; } - @NoSQLField + @MongoField public Gender getGender() { return gender; } @@ -59,7 +50,7 @@ public class Person extends AbstractNoSQLObject { this.gender = gender; } - @NoSQLField + @MongoField public List getGenders() { return genders; } @@ -68,7 +59,7 @@ public class Person extends AbstractNoSQLObject { this.genders = genders; } - @NoSQLField + @MongoField public List getKids() { return kids; } @@ -77,7 +68,7 @@ public class Person extends AbstractNoSQLObject { this.kids = kids; } - @NoSQLField + @MongoField public List
getAddresses() { return addresses; } @@ -86,7 +77,7 @@ public class Person extends AbstractNoSQLObject { this.addresses = addresses; } - @NoSQLField + @MongoField public Address getMainAddress() { return mainAddress; } @@ -95,6 +86,23 @@ public class Person extends AbstractNoSQLObject { this.mainAddress = mainAddress; } + @MongoField + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public void addAttribute(String key, String value) { + attributes.put(key, value); + } + + public void removeAttribute(String key) { + attributes.remove(key); + } + public static enum Gender { MALE, FEMALE } diff --git a/model/pom.xml b/model/pom.xml index ab454cad1e..41d5be1f00 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -37,6 +37,7 @@ api jpa - + mongo + tests diff --git a/model/tests/pom.xml b/model/tests/pom.xml new file mode 100644 index 0000000000..c4d733068f --- /dev/null +++ b/model/tests/pom.xml @@ -0,0 +1,69 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-2-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-model-tests + Keycloak Model Tests + + + + + org.keycloak + keycloak-services + ${project.version} + compile + + + junit + junit + compile + + + org.codehaus.jackson + jackson-core-asl + compile + + + org.codehaus.jackson + jackson-mapper-asl + compile + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + package-tests-jar + package + + test-jar + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + + diff --git a/model/tests/src/test/java/org/keycloak/model/test/AbstractModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/AbstractModelTest.java new file mode 100644 index 0000000000..1f2fda1be3 --- /dev/null +++ b/model/tests/src/test/java/org/keycloak/model/test/AbstractModelTest.java @@ -0,0 +1,55 @@ +package org.keycloak.model.test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.jboss.resteasy.logging.Logger; +import org.junit.After; +import org.junit.Before; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.resources.KeycloakApplication; +import org.keycloak.util.JsonSerialization; + +/** + * @author Marek Posolda + */ +public class AbstractModelTest { + + private final Logger log = Logger.getLogger(getClass()); + + protected KeycloakSessionFactory factory; + protected KeycloakSession identitySession; + protected RealmManager realmManager; + + @Before + public void before() throws Exception { + factory = KeycloakApplication.createSessionFactory(); + identitySession = factory.createSession(); + identitySession.getTransaction().begin(); + realmManager = new RealmManager(identitySession); + } + + @After + public void after() throws Exception { + identitySession.getTransaction().commit(); + identitySession.close(); + factory.close(); + } + + public static RealmRepresentation loadJson(String path) throws IOException { + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + int c; + while ((c = is.read()) != -1) { + os.write(c); + } + byte[] bytes = os.toByteArray(); + System.out.println(new String(bytes)); + + return JsonSerialization.readValue(bytes, RealmRepresentation.class); + } +} diff --git a/services/src/test/java/org/keycloak/test/AdapterTest.java b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java similarity index 92% rename from services/src/test/java/org/keycloak/test/AdapterTest.java rename to model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java index 9f49a98dc7..6bd1f07bd1 100755 --- a/services/src/test/java/org/keycloak/test/AdapterTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java @@ -1,4 +1,4 @@ -package org.keycloak.test; +package org.keycloak.model.test; import org.junit.Assert; import org.junit.FixMethodOrder; @@ -17,47 +17,30 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.OAuthClientManager; import org.keycloak.services.managers.RealmManager; -import org.keycloak.test.common.AbstractKeycloakTest; -import org.keycloak.test.common.SessionFactoryTestContext; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.StringTokenizer; /** * @author Bill Burke * @version $Revision: 1 $ */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class AdapterTest extends AbstractKeycloakTest { +public class AdapterTest extends AbstractModelTest { private RealmModel realmModel; - public AdapterTest(SessionFactoryTestContext testContext) { - super(testContext); - } - @Test public void installTest() throws Exception { new ApplianceBootstrap().bootstrap(identitySession); } - @Test - public void testMe() { - String hello = "Bill Burke"; - StringTokenizer tokenizer = new StringTokenizer(hello, " "); - while (tokenizer.hasMoreTokens()) { - System.out.println("token: " + tokenizer.nextToken()); - } - } - - @Test public void test1CreateRealm() throws Exception { - realmModel = getRealmManager().createRealm("JUGGLER"); + realmModel = realmManager.createRealm("JUGGLER"); realmModel.setAccessCodeLifespan(100); realmModel.setAccessCodeLifespanUserAction(600); realmModel.setEnabled(true); @@ -69,7 +52,7 @@ public class AdapterTest extends AbstractKeycloakTest { realmModel.addDefaultRole("foo"); System.out.println(realmModel.getId()); - realmModel = getRealmManager().getRealm(realmModel.getId()); + realmModel = realmManager.getRealm(realmModel.getId()); Assert.assertNotNull(realmModel); Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100); Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction()); @@ -85,7 +68,7 @@ public class AdapterTest extends AbstractKeycloakTest { @Test public void testRealmListing() throws Exception { - realmModel = getRealmManager().createRealm("JUGGLER"); + realmModel = realmManager.createRealm("JUGGLER"); realmModel.setAccessCodeLifespan(100); realmModel.setAccessCodeLifespanUserAction(600); realmModel.setEnabled(true); @@ -97,7 +80,7 @@ public class AdapterTest extends AbstractKeycloakTest { realmModel.addDefaultRole("foo"); System.out.println(realmModel.getId()); - realmModel = getRealmManager().getRealm(realmModel.getId()); + realmModel = realmManager.getRealm(realmModel.getId()); Assert.assertNotNull(realmModel); Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100); Assert.assertEquals(600, realmModel.getAccessCodeLifespanUserAction()); @@ -300,7 +283,7 @@ public class AdapterTest extends AbstractKeycloakTest { user3.setEmail("knut@redhat.com"); } - RealmManager adapter = getRealmManager(); + RealmManager adapter = realmManager; { List userModels = adapter.searchUsers("total junk query", realmModel); diff --git a/services/src/test/java/org/keycloak/test/ApplicationModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/ApplicationModelTest.java similarity index 72% rename from services/src/test/java/org/keycloak/test/ApplicationModelTest.java rename to model/tests/src/test/java/org/keycloak/model/test/ApplicationModelTest.java index 1942b41f01..d2e460c1c7 100755 --- a/services/src/test/java/org/keycloak/test/ApplicationModelTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ApplicationModelTest.java @@ -1,19 +1,14 @@ -package org.keycloak.test; +package org.keycloak.model.test; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.keycloak.models.ApplicationModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.services.managers.ApplicationManager; -import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.resources.KeycloakApplication; import java.util.Iterator; import java.util.List; @@ -21,24 +16,17 @@ import java.util.List; /** * @author Stian Thorgersen */ -public class ApplicationModelTest extends AbstractKeycloakServerTest { - private KeycloakSessionFactory factory; - private KeycloakSession identitySession; - private RealmManager manager; +public class ApplicationModelTest extends AbstractModelTest { private ApplicationModel application; private RealmModel realm; private ApplicationManager appManager; @Before public void before() throws Exception { - factory = KeycloakApplication.createSessionFactory(); - identitySession = factory.createSession(); - identitySession.getTransaction().begin(); - manager = new RealmManager(identitySession); + super.before(); + appManager = new ApplicationManager(realmManager); - appManager = new ApplicationManager(manager); - - realm = manager.createRealm("original"); + realm = realmManager.createRealm("original"); application = realm.addApplication("application"); application.setBaseUrl("http://base"); application.setManagementUrl("http://management"); @@ -57,16 +45,9 @@ public class ApplicationModelTest extends AbstractKeycloakServerTest { application.updateApplication(); } - @After - public void after() throws Exception { - identitySession.getTransaction().commit(); - identitySession.close(); - factory.close(); - } - @Test public void persist() { - RealmModel persisted = manager.getRealm(realm.getId()); + RealmModel persisted = realmManager.getRealm(realm.getId()); assertEquals(application, persisted.getApplicationNameMap().get("app-name")); } @@ -75,7 +56,7 @@ public class ApplicationModelTest extends AbstractKeycloakServerTest { public void json() { ApplicationRepresentation representation = appManager.toRepresentation(application); - RealmModel realm = manager.createRealm("copy"); + RealmModel realm = realmManager.createRealm("copy"); ApplicationModel copy = appManager.createApplication(realm, representation); assertEquals(application, copy); diff --git a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java similarity index 92% rename from services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java rename to model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java index 527838ddd3..324802b4d1 100755 --- a/services/src/test/java/org/keycloak/services/managers/AuthenticationManagerTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AuthenticationManagerTest.java @@ -1,4 +1,4 @@ -package org.keycloak.services.managers; +package org.keycloak.model.test; import org.junit.Assert; import org.junit.Before; @@ -9,15 +9,14 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; -import org.keycloak.test.common.AbstractKeycloakTest; -import org.keycloak.test.common.SessionFactoryTestContext; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import java.util.UUID; -public class AuthenticationManagerTest extends AbstractKeycloakTest { +public class AuthenticationManagerTest extends AbstractModelTest { private AuthenticationManager am; private MultivaluedMap formData; @@ -25,10 +24,6 @@ public class AuthenticationManagerTest extends AbstractKeycloakTest { private RealmModel realm; private UserModel user; - public AuthenticationManagerTest(SessionFactoryTestContext testContext) { - super(testContext); - } - @Test public void authForm() { AuthenticationStatus status = am.authenticateForm(realm, user, formData); @@ -126,7 +121,7 @@ public class AuthenticationManagerTest extends AbstractKeycloakTest { @Before public void before() throws Exception { super.before(); - realm = getRealmManager().createRealm("Test"); + realm = realmManager.createRealm("Test"); realm.setAccessCodeLifespan(100); realm.setEnabled(true); realm.setName("Test"); diff --git a/model/tests/src/test/java/org/keycloak/model/test/CompositeRolesModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/CompositeRolesModelTest.java new file mode 100644 index 0000000000..1baf83e0a9 --- /dev/null +++ b/model/tests/src/test/java/org/keycloak/model/test/CompositeRolesModelTest.java @@ -0,0 +1,132 @@ +package org.keycloak.model.test; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.managers.RealmManager; + +/** + * @author Marek Posolda + */ +public class CompositeRolesModelTest extends AbstractModelTest { + + @Before + public void before() throws Exception { + super.before(); + RealmManager manager = realmManager; + RealmRepresentation rep = AbstractModelTest.loadJson("testcomposites.json"); + RealmModel realm = manager.createRealm("Test", rep.getRealm()); + manager.importRealm(rep, realm); + } + + @Test + public void testAppComposites() { + Set requestedRoles = getRequestedRoles("APP_COMPOSITE_APPLICATION", "APP_COMPOSITE_USER"); + + Assert.assertEquals(2, requestedRoles.size()); + assertContains("APP_ROLE_APPLICATION", "APP_ROLE_1", requestedRoles); + assertContains("realm", "REALM_ROLE_1", requestedRoles); + } + + @Test + public void testRealmAppComposites() { + Set requestedRoles = getRequestedRoles("APP_COMPOSITE_APPLICATION", "REALM_APP_COMPOSITE_USER"); + + Assert.assertEquals(1, requestedRoles.size()); + assertContains("APP_ROLE_APPLICATION", "APP_ROLE_1", requestedRoles); + } + + @Test + public void testRealmOnlyWithUserCompositeAppComposite() throws Exception { + Set requestedRoles = getRequestedRoles("REALM_COMPOSITE_1_APPLICATION", "REALM_COMPOSITE_1_USER"); + + Assert.assertEquals(1, requestedRoles.size()); + assertContains("realm", "REALM_COMPOSITE_1", requestedRoles); + } + + @Test + public void testRealmOnlyWithUserCompositeAppRole() throws Exception { + Set requestedRoles = getRequestedRoles("REALM_ROLE_1_APPLICATION", "REALM_COMPOSITE_1_USER"); + + Assert.assertEquals(1, requestedRoles.size()); + assertContains("realm", "REALM_ROLE_1", requestedRoles); + } + + @Test + public void testRealmOnlyWithUserRoleAppComposite() throws Exception { + Set requestedRoles = getRequestedRoles("REALM_COMPOSITE_1_APPLICATION", "REALM_ROLE_1_USER"); + + Assert.assertEquals(1, requestedRoles.size()); + assertContains("realm", "REALM_ROLE_1", requestedRoles); + } + + // TODO: more tests... + + // Same algorithm as in TokenManager.createAccessCode + private Set getRequestedRoles(String applicationName, String username) { + Set requestedRoles = new HashSet(); + + RealmModel realm = realmManager.getRealm("Test"); + UserModel user = realm.getUser(username); + ApplicationModel application = realm.getApplicationByName(applicationName); + + Set roleMappings = realm.getRoleMappings(user); + Set scopeMappings = realm.getScopeMappings(application.getApplicationUser()); + Set appRoles = application.getRoles(); + if (appRoles != null) scopeMappings.addAll(appRoles); + + for (RoleModel role : roleMappings) { + if (role.getContainer().equals(application)) requestedRoles.add(role); + for (RoleModel desiredRole : scopeMappings) { + Set visited = new HashSet(); + applyScope(role, desiredRole, visited, requestedRoles); + } + } + + return requestedRoles; + } + + private static void applyScope(RoleModel role, RoleModel scope, Set visited, Set requested) { + if (visited.contains(scope)) return; + visited.add(scope); + if (role.hasRole(scope)) { + requested.add(scope); + return; + } + if (!scope.isComposite()) return; + + for (RoleModel contained : scope.getComposites()) { + applyScope(role, contained, visited, requested); + } + } + + private RoleModel getRole(String appName, String roleName) { + RealmModel realm = realmManager.getRealm("Test"); + if ("realm".equals(appName)) { + return realm.getRole(roleName); + } else { + return realm.getApplicationByName(appName).getRole(roleName); + } + } + + private void assertContains(String appName, String roleName, Set requestedRoles) { + RoleModel expectedRole = getRole(appName, roleName); + + Assert.assertTrue(requestedRoles.contains(expectedRole)); + + // Check if requestedRole has correct role container + for (RoleModel role : requestedRoles) { + if (role.equals(expectedRole)) { + Assert.assertEquals(role.getContainer(), expectedRole.getContainer()); + } + } + } +} diff --git a/services/src/test/java/org/keycloak/test/ImportTest.java b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java similarity index 52% rename from services/src/test/java/org/keycloak/test/ImportTest.java rename to model/tests/src/test/java/org/keycloak/model/test/ImportTest.java index d7ad50cd8d..6a4a908f9a 100755 --- a/services/src/test/java/org/keycloak/test/ImportTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java @@ -1,10 +1,11 @@ -package org.keycloak.test; +package org.keycloak.model.test; import org.junit.Assert; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RoleModel; @@ -12,10 +13,9 @@ import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.RealmManager; -import org.keycloak.test.common.AbstractKeycloakTest; -import org.keycloak.test.common.SessionFactoryTestContext; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -23,16 +23,12 @@ import java.util.Set; * @version $Revision: 1 $ */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ImportTest extends AbstractKeycloakTest { - - public ImportTest(SessionFactoryTestContext testContext) { - super(testContext); - } +public class ImportTest extends AbstractModelTest { @Test public void install() throws Exception { - RealmManager manager = getRealmManager(); - RealmRepresentation rep = AbstractKeycloakServerTest.loadJson("testrealm.json"); + RealmManager manager = realmManager; + RealmRepresentation rep = AbstractModelTest.loadJson("testrealm.json"); RealmModel realm = manager.createRealm("demo", rep.getRealm()); manager.importRealm(rep, realm); @@ -57,14 +53,68 @@ public class ImportTest extends AbstractKeycloakTest { List resources = realm.getApplications(); Assert.assertEquals(3, resources.size()); - // Test scope relationship - ApplicationModel application = realm.getApplicationNameMap().get("Application"); - UserModel oauthClient = realm.getUser("oauthclient"); + // Test applications imported + ApplicationModel application = realm.getApplicationByName("Application"); + ApplicationModel otherApp = realm.getApplicationByName("OtherApp"); + ApplicationModel accountApp = realm.getApplicationByName(Constants.ACCOUNT_APPLICATION); + ApplicationModel nonExisting = realm.getApplicationByName("NonExisting"); Assert.assertNotNull(application); + Assert.assertNotNull(otherApp); + Assert.assertNull(nonExisting); + Map apps = realm.getApplicationNameMap(); + Assert.assertEquals(3, apps.size()); + Assert.assertTrue(apps.values().contains(application)); + Assert.assertTrue(apps.values().contains(otherApp)); + Assert.assertTrue(apps.values().contains(accountApp)); + realm.getApplications().containsAll(apps.values()); + + // Test finding applications by ID + Assert.assertNull(realm.getApplicationById("982734")); + Assert.assertEquals(application, realm.getApplicationById(application.getId())); + + + // Test role mappings + UserModel admin = realm.getUser("admin"); + Set allRoles = realm.getRoleMappings(admin); + Assert.assertEquals(5, allRoles.size()); + Assert.assertTrue(allRoles.contains(realm.getRole("admin"))); + Assert.assertTrue(allRoles.contains(application.getRole("app-admin"))); + Assert.assertTrue(allRoles.contains(otherApp.getRole("otherapp-admin"))); + Assert.assertTrue(allRoles.contains(accountApp.getRole(Constants.ACCOUNT_PROFILE_ROLE))); + Assert.assertTrue(allRoles.contains(accountApp.getRole(Constants.ACCOUNT_MANAGE_ROLE))); + + UserModel wburke = realm.getUser("wburke"); + allRoles = realm.getRoleMappings(wburke); + Assert.assertEquals(4, allRoles.size()); + Assert.assertFalse(allRoles.contains(realm.getRole("admin"))); + Assert.assertTrue(allRoles.contains(application.getRole("app-user"))); + Assert.assertTrue(allRoles.contains(otherApp.getRole("otherapp-user"))); + + Assert.assertEquals(0, realm.getRealmRoleMappings(wburke).size()); + + Set realmRoles = realm.getRealmRoleMappings(admin); + Assert.assertEquals(1, realmRoles.size()); + Assert.assertEquals("admin", realmRoles.iterator().next().getName()); + + Set appRoles = application.getApplicationRoleMappings(admin); + Assert.assertEquals(1, appRoles.size()); + Assert.assertEquals("app-admin", appRoles.iterator().next().getName()); + + + // Test scope relationship + UserModel oauthClient = realm.getUser("oauthclient"); Assert.assertNotNull(oauthClient); + Set allScopes = realm.getScopeMappings(oauthClient); + Assert.assertEquals(2, allScopes.size()); + Assert.assertTrue(allScopes.contains(realm.getRole("admin"))); + Assert.assertTrue(allScopes.contains(application.getRole("app-user"))); + + Set realmScopes = realm.getRealmScopeMappings(oauthClient); + Assert.assertTrue(realmScopes.contains(realm.getRole("admin"))); + Set appScopes = application.getApplicationScopeMappings(oauthClient); - RoleModel appUserRole = application.getRole("user"); - Assert.assertTrue(appScopes.contains(appUserRole)); + Assert.assertTrue(appScopes.contains(application.getRole("app-user"))); + // Test social linking UserModel socialUser = realm.getUser("mySocialUser"); @@ -87,12 +137,14 @@ public class ImportTest extends AbstractKeycloakTest { Assert.assertEquals(foundSocialUser.getLoginName(), socialUser.getLoginName()); Assert.assertNull(realm.getUserBySocialLink(new SocialLinkModel("facebook", "not-existing"))); + + } @Test public void install2() throws Exception { - RealmManager manager = getRealmManager(); - RealmRepresentation rep = AbstractKeycloakServerTest.loadJson("testrealm-demo.json"); + RealmManager manager = realmManager; + RealmRepresentation rep = AbstractModelTest.loadJson("testrealm-demo.json"); RealmModel realm = manager.createRealm("demo", rep.getRealm()); manager.importRealm(rep, realm); diff --git a/services/src/test/java/org/keycloak/test/ModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/ModelTest.java similarity index 72% rename from services/src/test/java/org/keycloak/test/ModelTest.java rename to model/tests/src/test/java/org/keycloak/model/test/ModelTest.java index cdec0040be..bdc7d3b299 100755 --- a/services/src/test/java/org/keycloak/test/ModelTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ModelTest.java @@ -1,46 +1,22 @@ -package org.keycloak.test; +package org.keycloak.model.test; -import org.junit.After; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.ModelToRepresentation; -import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.resources.KeycloakApplication; import java.util.HashMap; import java.util.Iterator; import java.util.List; -public class ModelTest extends AbstractKeycloakServerTest { - private KeycloakSessionFactory factory; - private KeycloakSession identitySession; - private RealmManager manager; - - @Before - public void before() throws Exception { - factory = KeycloakApplication.createSessionFactory(); - identitySession = factory.createSession(); - identitySession.getTransaction().begin(); - manager = new RealmManager(identitySession); - } - - @After - public void after() throws Exception { - identitySession.getTransaction().commit(); - identitySession.close(); - factory.close(); - } +public class ModelTest extends AbstractModelTest { @Test public void importExportRealm() { - RealmModel realm = manager.createRealm("original"); + RealmModel realm = realmManager.createRealm("original"); realm.setRegistrationAllowed(true); realm.setResetPasswordAllowed(true); realm.setSocial(true); @@ -62,10 +38,10 @@ public class ModelTest extends AbstractKeycloakServerTest { HashMap social = new HashMap(); social.put("google.key", "1234"); social.put("google.secret", "5678"); - realm.setSmtpConfig(social); + realm.setSocialConfig(social); - RealmModel peristed = manager.getRealm(realm.getId()); - assertEquals(realm, peristed); + RealmModel persisted = realmManager.getRealm(realm.getId()); + assertEquals(realm, persisted); RealmModel copy = importExport(realm, "copy"); assertEquals(realm, copy); @@ -103,9 +79,9 @@ public class ModelTest extends AbstractKeycloakServerTest { private RealmModel importExport(RealmModel src, String copyName) { RealmRepresentation representation = ModelToRepresentation.toRepresentation(src); - RealmModel copy = manager.createRealm(copyName); - manager.importRealm(representation, copy); - return manager.getRealm(copy.getId()); + RealmModel copy = realmManager.createRealm(copyName); + realmManager.importRealm(representation, copy); + return realmManager.getRealm(copy.getId()); } } diff --git a/services/src/test/java/org/keycloak/test/UserModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/UserModelTest.java similarity index 78% rename from services/src/test/java/org/keycloak/test/UserModelTest.java rename to model/tests/src/test/java/org/keycloak/model/test/UserModelTest.java index 338af77230..6567e19971 100755 --- a/services/src/test/java/org/keycloak/test/UserModelTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/UserModelTest.java @@ -1,17 +1,12 @@ -package org.keycloak.test; +package org.keycloak.model.test; -import org.junit.After; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.resources.KeycloakApplication; import java.util.Iterator; import java.util.List; @@ -19,29 +14,11 @@ import java.util.List; /** * @author Stian Thorgersen */ -public class UserModelTest extends AbstractKeycloakServerTest { - private KeycloakSessionFactory factory; - private KeycloakSession identitySession; - private RealmManager manager; - - @Before - public void before() throws Exception { - factory = KeycloakApplication.createSessionFactory(); - identitySession = factory.createSession(); - identitySession.getTransaction().begin(); - manager = new RealmManager(identitySession); - } - - @After - public void after() throws Exception { - identitySession.getTransaction().commit(); - identitySession.close(); - factory.close(); - } +public class UserModelTest extends AbstractModelTest { @Test public void persistUser() { - RealmModel realm = manager.createRealm("original"); + RealmModel realm = realmManager.createRealm("original"); UserModel user = realm.addUser("user"); user.setFirstName("first-name"); user.setLastName("last-name"); @@ -56,14 +33,14 @@ public class UserModelTest extends AbstractKeycloakServerTest { user.addWebOrigin("origin-1"); user.addWebOrigin("origin-2"); - UserModel persisted = manager.getRealm(realm.getId()).getUser("user"); + UserModel persisted = realmManager.getRealm(realm.getId()).getUser("user"); assertEquals(user, persisted); } @Test public void webOriginSetTest() { - RealmModel realm = manager.createRealm("original"); + RealmModel realm = realmManager.createRealm("original"); UserModel user = realm.addUser("user"); Assert.assertTrue(user.getWebOrigins().isEmpty()); @@ -83,7 +60,7 @@ public class UserModelTest extends AbstractKeycloakServerTest { @Test public void testUserRequiredActions() throws Exception { - RealmModel realm = manager.createRealm("original"); + RealmModel realm = realmManager.createRealm("original"); UserModel user = realm.addUser("user"); Assert.assertTrue(user.getRequiredActions().isEmpty()); @@ -91,7 +68,7 @@ public class UserModelTest extends AbstractKeycloakServerTest { user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); String id = realm.getId(); commit(); - realm = manager.getRealm(id); + realm = realmManager.getRealm(id); user = realm.getUser("user"); Assert.assertEquals(1, user.getRequiredActions().size()); @@ -127,7 +104,7 @@ public class UserModelTest extends AbstractKeycloakServerTest { identitySession.close(); identitySession = factory.createSession(); identitySession.getTransaction().begin(); - manager = new RealmManager(identitySession); + realmManager = new RealmManager(identitySession); } public static void assertEquals(UserModel expected, UserModel actual) { diff --git a/model/tests/src/test/resources/testcomposites.json b/model/tests/src/test/resources/testcomposites.json new file mode 100644 index 0000000000..73e4300002 --- /dev/null +++ b/model/tests/src/test/resources/testcomposites.json @@ -0,0 +1,231 @@ +{ + "id": "Test", + "realm": "Test", + "enabled": true, + "tokenLifespan": 600, + "accessCodeLifespan": 600, + "accessCodeLifespanUserAction": 600, + "sslNotRequired": true, + "registrationAllowed": true, + "resetPasswordAllowed": true, + "requiredCredentials": [ "password" ], + "requiredApplicationCredentials": [ "password" ], + "requiredOAuthClientCredentials": [ "password" ], + "smtpServer": { + "from": "auto@keycloak.org", + "host": "localhost", + "port":"3025" + }, + "users" : [ + { + "username" : "REALM_COMPOSITE_1_USER", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + }, + { + "username" : "REALM_ROLE_1_USER", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + }, + { + "username" : "REALM_APP_COMPOSITE_USER", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + }, + { + "username" : "REALM_APP_ROLE_USER", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + }, + { + "username" : "APP_COMPOSITE_USER", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + } + ], + "oauthClients" : [ + { + "name" : "third-party", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "password" } + ] + } + ], + "roleMappings": [ + { + "username": "REALM_COMPOSITE_1_USER", + "roles": ["REALM_COMPOSITE_1"] + }, + { + "username": "REALM_ROLE_1_USER", + "roles": ["REALM_ROLE_1"] + }, + { + "username": "REALM_APP_COMPOSITE_USER", + "roles": ["REALM_APP_COMPOSITE_ROLE"] + }, + { + "username": "APP_COMPOSITE_USER", + "roles": ["REALM_APP_COMPOSITE_ROLE", "REALM_COMPOSITE_1"] + } + ], + "scopeMappings": [ + { + "username": "REALM_COMPOSITE_1_APPLICATION", + "roles": ["REALM_COMPOSITE_1"] + }, + { + "username": "REALM_ROLE_1_APPLICATION", + "roles": ["REALM_ROLE_1"] + } + ], + "applications": [ + { + "name": "REALM_COMPOSITE_1_APPLICATION", + "enabled": true, + "baseUrl": "http://localhost:8081/app", + "adminUrl": "http://localhost:8081/app/logout", + "credentials": [ + { + "type": "password", + "value": "password" + } + ] + }, + { + "name": "REALM_ROLE_1_APPLICATION", + "enabled": true, + "baseUrl": "http://localhost:8081/app", + "adminUrl": "http://localhost:8081/app/logout", + "credentials": [ + { + "type": "password", + "value": "password" + } + ] + }, + { + "name": "APP_ROLE_APPLICATION", + "enabled": true, + "baseUrl": "http://localhost:8081/app", + "adminUrl": "http://localhost:8081/app/logout", + "credentials": [ + { + "type": "password", + "value": "password" + } + ] + }, + { + "name": "APP_COMPOSITE_APPLICATION", + "enabled": true, + "baseUrl": "http://localhost:8081/app", + "adminUrl": "http://localhost:8081/app/logout", + "credentials": [ + { + "type": "password", + "value": "password" + } + ] + } + ], + "roles" : { + "realm" : [ + { + "name": "REALM_ROLE_1" + }, + { + "name": "REALM_ROLE_2" + }, + { + "name": "REALM_ROLE_3" + }, + { + "name": "REALM_COMPOSITE_1", + "composites": { + "realm": ["REALM_ROLE_1"] + } + }, + { + "name": "REALM_APP_COMPOSITE_ROLE", + "composites": { + "application": { + "APP_ROLE_APPLICATION" :[ + "APP_ROLE_1" + ] + } + } + } + ], + "application" : { + "APP_ROLE_APPLICATION" : [ + { + "name": "APP_ROLE_1" + }, + { + "name": "APP_ROLE_2" + } + ], + "APP_COMPOSITE_APPLICATION" : [ + { + "name": "APP_COMPOSITE_ROLE", + "composites": { + "realm" : [ + "REALM_ROLE_1", + "REALM_ROLE_2", + "REALM_ROLE_3" + ], + "application": { + "APP_ROLE_APPLICATION" :[ + "APP_ROLE_1" + ] + } + } + }, + { + "name": "APP_ROLE_2" + } + ] + } + + }, + + "applicationRoleMappings": { + "APP_ROLE_APPLICATION": [ + { + "username": "REALM_APP_ROLE_USER", + "roles": ["APP_ROLE_2"] + } + ] + }, + "applicationScopeMappings": { + "APP_ROLE_APPLICATION": [ + { + "username": "APP_COMPOSITE_APPLICATION", + "roles": ["APP_ROLE_2"] + } + ] + } +} \ No newline at end of file diff --git a/services/src/test/resources/testrealm-demo.json b/model/tests/src/test/resources/testrealm-demo.json similarity index 100% rename from services/src/test/resources/testrealm-demo.json rename to model/tests/src/test/resources/testrealm-demo.json diff --git a/services/src/test/resources/testrealm.json b/model/tests/src/test/resources/testrealm.json similarity index 79% rename from services/src/test/resources/testrealm.json rename to model/tests/src/test/resources/testrealm.json index 76f90a97c3..16ccf469d2 100755 --- a/services/src/test/resources/testrealm.json +++ b/model/tests/src/test/resources/testrealm.json @@ -43,16 +43,6 @@ } ] }, - { - "username": "oauthclient", - "enabled": true, - "credentials": [ - { - "type": "password", - "value": "clientpassword" - } - ] - }, { "username": "mySocialUser", "enabled": true @@ -88,6 +78,16 @@ } ], + "oauthClients" : [ + { + "name" : "oauthclient", + "enabled": true, + "credentials" : [ + { "type" : "password", + "value" : "clientpassword" } + ] + } + ], "roles" : { "realm" : [ { @@ -97,18 +97,18 @@ "application" : { "Application" : [ { - "name": "admin" + "name": "app-admin" }, { - "name": "user" + "name": "app-user" } ], "OtherApp" : [ { - "name": "admin" + "name": "otherapp-admin" }, { - "name": "user" + "name": "otherapp-user" } ] } @@ -119,25 +119,31 @@ "roles": ["admin"] } ], + "scopeMappings": [ + { + "username": "oauthclient", + "roles": ["admin"] + } + ], "applicationRoleMappings": { "Application": [ { "username": "wburke", - "roles": ["user"] + "roles": ["app-user"] }, { "username": "admin", - "roles": ["admin"] + "roles": ["app-admin"] } ], "OtherApp": [ { "username": "wburke", - "roles": ["user"] + "roles": ["otherapp-user"] }, { "username": "admin", - "roles": ["admin"] + "roles": ["otherapp-admin"] } ] }, @@ -145,7 +151,7 @@ "Application": [ { "username": "oauthclient", - "roles": ["user"] + "roles": ["app-user"] } ] diff --git a/pom.xml b/pom.xml index b1aaefd6e8..91b0818743 100755 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ 3.0.6.Final 1.0.0.Beta30 2.5.0.Beta6 - 2.11.2 + 2.11.3 3.1.1.GA 1.2.0.Beta1 1.0.1.Final @@ -313,11 +313,6 @@ mongo-java-driver 2.11.2 - - de.flapdoodle.embed - de.flapdoodle.embed.mongo - 1.27 - org.apache.jmeter ApacheJMeter_java @@ -395,7 +390,7 @@ org.apache.maven.plugins maven-surefire-plugin - + 2.16 once -Xms512m -Xmx512m @@ -479,6 +474,11 @@ exec-maven-plugin 1.2.1 + + com.github.joelittlejohn.embedmongo + embedmongo-maven-plugin + 0.1.10 + diff --git a/services/pom.xml b/services/pom.xml index d97d6b40c0..969dc68737 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -56,27 +56,6 @@ keycloak-jaxrs-oauth-client ${project.version} - - - org.keycloak - keycloak-model-jpa - ${project.version} - test - - - - org.keycloak keycloak-social-core @@ -127,21 +106,6 @@ resteasy-multipart-provider provided - - org.jboss.resteasy - resteasy-undertow - test - - - io.undertow - undertow-servlet - test - - - io.undertow - undertow-core - test - org.codehaus.jackson jackson-core-asl @@ -171,21 +135,6 @@ junit test - - org.hibernate.javax.persistence - hibernate-jpa-2.0-api - test - - - com.h2database - h2 - test - - - org.hibernate - hibernate-entitymanager - test - com.icegreen greenmail diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 60ee3b8cd1..bc937d7af9 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -11,6 +11,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.resources.AccountService; @@ -41,7 +42,7 @@ public class AuthenticationManager { public SkeletonKeyToken createIdentityToken(RealmModel realm, String username) { SkeletonKeyToken token = new SkeletonKeyToken(); - token.id(RealmManager.generateId()); + token.id(KeycloakModelUtils.generateId()); token.issuedNow(); token.principal(username); token.audience(realm.getName()); diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 461159983a..b9c0e64bf6 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -13,6 +13,7 @@ import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.ApplicationRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.OAuthClientRepresentation; @@ -44,11 +45,6 @@ import java.util.concurrent.atomic.AtomicLong; */ public class RealmManager { protected static final Logger logger = Logger.getLogger(RealmManager.class); - private static AtomicLong counter = new AtomicLong(1); - - public static String generateId() { - return counter.getAndIncrement() + "-" + System.currentTimeMillis(); - } protected KeycloakSession identitySession; @@ -73,7 +69,7 @@ public class RealmManager { } public RealmModel createRealm(String id, String name) { - if (id == null) id = generateId(); + if (id == null) id = KeycloakModelUtils.generateId(); RealmModel realm = identitySession.createRealm(id, name); realm.setName(name); realm.addRole(Constants.APPLICATION_ROLE); @@ -166,7 +162,7 @@ public class RealmManager { public RealmModel importRealm(RealmRepresentation rep, UserModel realmCreator) { String id = rep.getId(); if (id == null) { - id = generateId(); + id = KeycloakModelUtils.generateId(); } RealmModel realm = createRealm(id, rep.getRealm()); importRealm(rep, realm); diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java index 87d4025feb..a7eeb86102 100755 --- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java +++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java @@ -3,9 +3,11 @@ package org.keycloak.services.managers; import org.jboss.resteasy.logging.Logger; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.ApplicationModel; +import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.SkeletonKeyScope; import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.util.Base64Url; @@ -14,6 +16,7 @@ import org.keycloak.util.JsonSerialization; import javax.ws.rs.core.MultivaluedMap; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -132,7 +135,7 @@ public class TokenManager { protected SkeletonKeyToken initToken(RealmModel realm, UserModel client, UserModel user) { SkeletonKeyToken token = new SkeletonKeyToken(); - token.id(RealmManager.generateId()); + token.id(KeycloakModelUtils.generateId()); token.principal(user.getLoginName()); token.audience(realm.getName()); token.issuedNow(); @@ -219,7 +222,7 @@ public class TokenManager { public SkeletonKeyToken createAccessToken(RealmModel realm, UserModel user) { SkeletonKeyToken token = new SkeletonKeyToken(); - token.id(RealmManager.generateId()); + token.id(KeycloakModelUtils.generateId()); token.issuedNow(); token.principal(user.getLoginName()); token.audience(realm.getName()); 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 6c89c12d16..83bdaca8dd 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -8,6 +8,7 @@ import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.admin.AdminService; +import org.keycloak.models.utils.ModelProviderUtils; import javax.servlet.ServletContext; import javax.ws.rs.core.Application; @@ -15,7 +16,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.HashSet; -import java.util.ServiceLoader; import java.util.Set; /** @@ -26,9 +26,6 @@ public class KeycloakApplication extends Application { private static final Logger log = Logger.getLogger(KeycloakApplication.class); - private static final String MODEL_PROVIDER = "keycloak.model"; - private static final String DEFAULT_MODEL_PROVIDER = "jpa"; - protected Set singletons = new HashSet(); protected Set> classes = new HashSet>(); @@ -73,28 +70,7 @@ public class KeycloakApplication extends Application { public static KeycloakSessionFactory createSessionFactory() { - ServiceLoader providers = ServiceLoader.load(ModelProvider.class); - String configuredProvider = System.getProperty(MODEL_PROVIDER); - ModelProvider provider = null; - - if (configuredProvider != null) { - for (ModelProvider p : providers) { - if (p.getId().equals(configuredProvider)) { - provider = p; - } - } - } else { - for (ModelProvider p : providers) { - if (provider == null) { - provider = p; - } - - if (p.getId().equals(DEFAULT_MODEL_PROVIDER)) { - provider = p; - break; - } - } - } + ModelProvider provider = ModelProviderUtils.getConfiguredModelProvider(); if (provider != null) { log.debug("Model provider: " + provider.getId()); diff --git a/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java b/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java deleted file mode 100755 index 583a8c8d1f..0000000000 --- a/services/src/main/java/org/keycloak/services/utils/PropertiesManager.java +++ /dev/null @@ -1,88 +0,0 @@ -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"; - public static final String SESSION_FACTORY_JPA = "jpa"; - - 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_JPA); - } - - public static void setSessionFactoryType(String sessionFactoryType) { - System.setProperty(SESSION_FACTORY, sessionFactoryType); - } - - public static void setDefaultSessionFactoryType() { - System.setProperty(SESSION_FACTORY, SESSION_FACTORY_JPA); - } - - public static boolean isMongoSessionFactory() { - return getSessionFactoryType().equals(SESSION_FACTORY_MONGO); - } - - public static boolean isPicketlinkSessionFactory() { - return getSessionFactoryType().equals(SESSION_FACTORY_PICKETLINK); - } - - public static boolean isJpaSessionFactory() { - return getSessionFactoryType().equals(SESSION_FACTORY_JPA); - } - - - 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/test/AbstractKeycloakServerTest.java b/services/src/test/java/org/keycloak/test/AbstractKeycloakServerTest.java deleted file mode 100755 index 96dbd49dc3..0000000000 --- a/services/src/test/java/org/keycloak/test/AbstractKeycloakServerTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.keycloak.test; - -import io.undertow.servlet.Servlets; -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.FilterInfo; -import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; -import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; -import org.jboss.resteasy.spi.ResteasyDeployment; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.keycloak.SkeletonKeyContextResolver; -import org.keycloak.util.JsonSerialization; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.services.filters.KeycloakSessionServletFilter; -import org.keycloak.services.resources.KeycloakApplication; - -import javax.servlet.DispatcherType; -import javax.ws.rs.client.Client; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class AbstractKeycloakServerTest { - public static UndertowJaxrsServer server; - public static ResteasyDeployment deployment; - public static Client client; - public static KeycloakApplication application; - - @BeforeClass - public static void undertowSetup() throws Exception { - deployment = new ResteasyDeployment(); - deployment.setApplicationClass(KeycloakApplication.class.getName()); - server = new UndertowJaxrsServer().start(); - DeploymentInfo di = server.undertowDeployment(deployment); - di.setClassLoader(AbstractKeycloakServerTest.class.getClassLoader()); - di.setContextPath("/"); - di.setDeploymentName("Keycloak"); - - FilterInfo filter = Servlets.filter("SessionFilter", KeycloakSessionServletFilter.class); - di.addFilter(filter); - di.addFilterUrlMapping("SessionFilter", "/*", DispatcherType.REQUEST); - server.deploy(di); - application = (KeycloakApplication) deployment.getApplication(); - client = new ResteasyClientBuilder().connectionPoolSize(10).build(); - client.register(SkeletonKeyContextResolver.class); - - } - - @AfterClass - public static void undertowShutdown() throws Exception { - server.stop(); - } - - public static RealmRepresentation loadJson(String path) throws IOException { - InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - int c; - while ((c = is.read()) != -1) { - os.write(c); - } - byte[] bytes = os.toByteArray(); - System.out.println(new String(bytes)); - - return JsonSerialization.readValue(bytes, RealmRepresentation.class); - } -} diff --git a/services/src/test/java/org/keycloak/test/common/AbstractKeycloakTest.java b/services/src/test/java/org/keycloak/test/common/AbstractKeycloakTest.java deleted file mode 100755 index 11512d79a5..0000000000 --- a/services/src/test/java/org/keycloak/test/common/AbstractKeycloakTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.keycloak.test.common; - -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; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author Marek Posolda - */ -@RunWith(Parameterized.class) -public abstract class AbstractKeycloakTest { - - protected static final SessionFactoryTestContext[] TEST_CONTEXTS; - - private final SessionFactoryTestContext testContext; - protected KeycloakSessionFactory factory; - protected KeycloakSession identitySession; - protected RealmManager realmManager; - - // STATIC METHODS - - static - { - // TODO: MongoDB disabled by default - TEST_CONTEXTS = new SessionFactoryTestContext[] { - //new PicketlinkSessionFactoryTestContext(), - new JpaSessionFactoryTestContext(), - // 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.createSessionFactory(); - 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/JpaSessionFactoryTestContext.java b/services/src/test/java/org/keycloak/test/common/JpaSessionFactoryTestContext.java deleted file mode 100755 index 94b80664cc..0000000000 --- a/services/src/test/java/org/keycloak/test/common/JpaSessionFactoryTestContext.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.keycloak.test.common; - -import org.keycloak.services.utils.PropertiesManager; - -/** - * @author Marek Posolda - */ -public class JpaSessionFactoryTestContext 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_JPA); - } -} diff --git a/services/src/test/java/org/keycloak/test/common/PicketlinkSessionFactoryTestContext.java b/services/src/test/java/org/keycloak/test/common/PicketlinkSessionFactoryTestContext.java deleted file mode 100755 index d152f3be34..0000000000 --- a/services/src/test/java/org/keycloak/test/common/PicketlinkSessionFactoryTestContext.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.keycloak.test.common; - -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 deleted file mode 100644 index a35cfd25c6..0000000000 --- a/services/src/test/java/org/keycloak/test/common/SessionFactoryTestContext.java +++ /dev/null @@ -1,17 +0,0 @@ -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/integration/README.md b/testsuite/integration/README.md index 642865706d..1b3ed3e802 100644 --- a/testsuite/integration/README.md +++ b/testsuite/integration/README.md @@ -8,6 +8,13 @@ The testsuite uses Sellenium. By default it uses the HtmlUnit WebDriver, but can To run the tests with Firefox add `-Dbrowser=firefox` or for Chrome add `-Dbrowser=chrome` +Mongo +----- + +The testsuite is executed with JPA model implementation with data saved in H2 database by default. To run testsuite with Mongo model, just add property `-Dkeycloak.model=mongo` when executing it. + +Note that this will automatically run embedded Mongo database on localhost/27018 and it will stop it after whole testsuite is finished. +So you don't need to have Mongo installed on your laptop to run mongo execution tests. Test utils ========== @@ -41,6 +48,16 @@ For example to use the example themes run the server with: **NOTE:** If `keycloak.theme.dir` is specified the default themes (base, rcue and keycloak) are loaded from the classpath +### Run server with Mongo model + +To start a Keycloak server with identity model data persisted in Mongo database instead of default JPA/H2 you can run: + + mvn exec:java -Pkeycloak-server -Dkeycloak.model=mongo + +By default it's using database `keycloak` on localhost/27017 and it uses already existing data from this DB (no cleanup of existing data during bootstrap). Assumption is that you already have DB running on localhost/27017 . Use system properties to configure things differently: + + mvn exec:java -Pkeycloak-server -Dkeycloak.model=mongo -Dkeycloak.mongo.host=localhost -Dkeycloak.mongo.port=27017 -Dkeycloak.mongo.db=keycloak -Dkeycloak.mongo.clearCollectionsOnStartup=false + TOTP codes ---------- diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 115dc5df85..d668ee8389 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -251,10 +251,22 @@ org.seleniumhq.selenium selenium-chrome-driver + + + + org.keycloak + keycloak-model-mongo + ${project.version} + org.mongodb mongo-java-driver + + org.picketlink + picketlink-common + + @@ -326,5 +338,84 @@ + + + mongo + + + keycloak.model + mongo + + + + + localhost + 27018 + keycloak + true + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test + integration-test + + test + + + + ${keycloak.mongo.host} + ${keycloak.mongo.port} + ${keycloak.mongo.db} + ${keycloak.mongo.clearOnStartup} + + + + + default-test + + true + + + + + + + + com.github.joelittlejohn.embedmongo + embedmongo-maven-plugin + + + start-mongodb + pre-integration-test + + start + + + ${keycloak.mongo.port} + file + ${project.build.directory}/mongodb.log + + + + stop-mongodb + post-integration-test + + stop + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java index 136a4003e6..571eda2bcf 100755 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java @@ -273,7 +273,7 @@ public class KeycloakServer { server.deploy(di); - factory = KeycloakApplication.createSessionFactory(); + factory = ((KeycloakApplication)deployment.getApplication()).getFactory(); setupDefaultRealm();