From ecadc92f40bf93767a5ef4f1f95a24c2669fa22e Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Mon, 16 Mar 2015 14:46:36 -0400 Subject: [PATCH 1/2] KEYCLOAK-1105 fix file profile in testsuite --- testsuite/integration/pom.xml | 15 +++++++-------- .../main/resources/META-INF/keycloak-server.json | 6 +----- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 7b50ede014..ac11fe35cf 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -413,13 +413,6 @@ file - - - - @@ -427,9 +420,15 @@ maven-surefire-plugin - **/ExportImportTest.java **/broker/*** + + file + file + none + none + ${project.build.directory} + diff --git a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json index 1060f21806..188db1e12e 100755 --- a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json @@ -8,11 +8,7 @@ }, "realm": { - "provider": "${keycloak.realm.provider:jpa}", - "file" : { - "directory" : ".", - "fileName" : "kcdata.json" - } + "provider": "${keycloak.realm.provider:jpa}" }, "user": { From 68b88b4baf2c202ee917f9f78fe8fea1a5ca7f13 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Tue, 17 Mar 2015 16:14:29 -0400 Subject: [PATCH 2/2] KEYCLOAK-1105 Refactor InMemoryModel to use a factory instead of a singleton. --- connections/file/pom.xml | 47 ++++ .../file/DefaultFileConnectionProvider.java | 86 ++++++ .../DefaultFileConnectionProviderFactory.java | 202 ++++++++++++++ .../file/FileConnectionProvider.java | 31 +++ .../file/FileConnectionProviderFactory.java | 25 ++ .../connections/file/FileConnectionSpi.java | 27 ++ .../connections/file/InMemoryModel.java | 99 +++++++ ...ections.file.FileConnectionProviderFactory | 1 + .../services/org.keycloak.provider.Spi | 1 + connections/pom.xml | 1 + distribution/modules/build.xml | 4 + .../keycloak-connections-file/main/module.xml | 19 ++ .../keycloak-model-file/main/module.xml | 4 +- .../keycloak/keycloak-server/main/module.xml | 1 + .../keycloak-services/main/module.xml | 1 + .../WEB-INF/jboss-deployment-structure.xml | 1 + model/file/pom.xml | 5 + .../models/file/FileRealmProvider.java | 18 +- .../models/file/FileRealmProviderFactory.java | 14 +- .../models/file/FileUserProvider.java | 10 +- .../models/file/FileUserProviderFactory.java | 4 +- .../keycloak/models/file/InMemoryModel.java | 263 ------------------ .../file/adapter/ApplicationAdapter.java | 2 +- .../models/file/adapter/RealmAdapter.java | 12 +- .../models/file/adapter/UserAdapter.java | 2 +- testsuite/integration/pom.xml | 1 + 26 files changed, 589 insertions(+), 292 deletions(-) create mode 100644 connections/file/pom.xml create mode 100644 connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProvider.java create mode 100755 connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java create mode 100644 connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProvider.java create mode 100644 connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProviderFactory.java create mode 100644 connections/file/src/main/java/org/keycloak/connections/file/FileConnectionSpi.java create mode 100644 connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java create mode 100644 connections/file/src/main/resources/META-INF/services/org.keycloak.connections.file.FileConnectionProviderFactory create mode 100644 connections/file/src/main/resources/META-INF/services/org.keycloak.provider.Spi create mode 100644 distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-file/main/module.xml delete mode 100644 model/file/src/main/java/org/keycloak/models/file/InMemoryModel.java diff --git a/connections/file/pom.xml b/connections/file/pom.xml new file mode 100644 index 0000000000..b506f8b786 --- /dev/null +++ b/connections/file/pom.xml @@ -0,0 +1,47 @@ + + + + keycloak-parent + org.keycloak + 1.2.0.Beta1-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-connections-file + Keycloak Connections File + + + + + org.keycloak + keycloak-export-import-api + ${project.version} + + + org.keycloak + keycloak-export-import-single-file + ${project.version} + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-model-api + ${project.version} + + + org.codehaus.jackson + jackson-mapper-asl + provided + + + org.jboss.logging + jboss-logging + + + diff --git a/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProvider.java b/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProvider.java new file mode 100644 index 0000000000..b821943901 --- /dev/null +++ b/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProvider.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.keycloak.connections.file; + +import org.keycloak.models.KeycloakSession; + +/** + * Provides the InMemoryModel and notifies the factory to save it when + * the session is done. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class DefaultFileConnectionProvider implements FileConnectionProvider { + + private final DefaultFileConnectionProviderFactory factory; + private final KeycloakSession session; + private final InMemoryModel inMemoryModel; + + private boolean isRollbackOnly = false; + + public DefaultFileConnectionProvider(DefaultFileConnectionProviderFactory factory, + KeycloakSession session, + InMemoryModel inMemoryModel) { + this.factory = factory; + this.session = session; + this.inMemoryModel = inMemoryModel; + } + + @Override + public InMemoryModel getModel() { + return inMemoryModel; + } + + @Override + public void sessionClosed(KeycloakSession session) { + factory.sessionClosed(session); + } + + @Override + public void close() { + } + + @Override + public void begin() { + } + + @Override + public void commit() { + factory.commit(session); + } + + @Override + public void rollback() { + factory.rollback(session); + } + + @Override + public void setRollbackOnly() { + isRollbackOnly = true; + } + + @Override + public boolean getRollbackOnly() { + return isRollbackOnly; + } + + @Override + public boolean isActive() { + return factory.isActive(session); + } + +} diff --git a/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java b/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java new file mode 100755 index 0000000000..0c9b5fe2c8 --- /dev/null +++ b/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java @@ -0,0 +1,202 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.keycloak.connections.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.exportimport.Strategy; +import org.keycloak.exportimport.util.ExportUtils; +import org.keycloak.exportimport.util.ImportUtils; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.util.JsonSerialization; + +/** + * This class dispenses a FileConnectionProvider to Keycloak sessions. It + * makes sure that only one InMemoryModel is provided for each session and it + * handles thread contention for the file where the model is read or saved. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class DefaultFileConnectionProviderFactory implements FileConnectionProviderFactory { + + protected static final Logger logger = Logger.getLogger(DefaultFileConnectionProviderFactory.class); + + private File kcdata; + private final Map allProviders = new HashMap(); + + @Override + public void init(Config.Scope config) { + String fileName = config.get("fileName"); + if (fileName == null) { + fileName = "keycloak-model.json"; + } + + String directory = config.get("directory"); + if (directory == null) { + directory = System.getProperty("jboss.server.data.dir"); + } + if (directory == null) { + directory = "."; + } + + kcdata = new File(directory, fileName); + } + + public void sessionClosed(KeycloakSession session) { + synchronized(allProviders) { + allProviders.remove(session); + //logger.info("Removed session " + session.hashCode()); + //logger.info("sessionClosed: Session count=" + allModels.size()); + } + } + + void readModelFile(KeycloakSession session) { + synchronized(allProviders) { + if (!kcdata.exists()) { + return; + } + + FileInputStream fis = null; + try { + fis = new FileInputStream(kcdata); + ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING); + } catch (IOException ioe) { + logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe); + } finally { + //logger.info("Read model file for session=" + session.hashCode()); + try { + if (fis != null) { + fis.close(); + } + } catch (IOException e) { + logger.error("Failed to close output stream.", e); + } + } + } + } + + void writeModelFile(KeycloakSession session) { + synchronized(allProviders) { + FileOutputStream outStream = null; + + try { + outStream = new FileOutputStream(kcdata); + exportModel(session, outStream); + } catch (IOException e) { + logger.error("Unable to write model file " + kcdata.getAbsolutePath(), e); + } finally { + //logger.info("Wrote model file for session=" + session.hashCode()); + try { + if (outStream != null) { + outStream.close(); + } + } catch (IOException e) { + logger.error("Failed to close output stream.", e); + } + } + } + } + + private void exportModel(KeycloakSession session, FileOutputStream outStream) throws IOException { + List realms = session.realms().getRealms(); + List reps = new ArrayList(); + for (RealmModel realm : realms) { + reps.add(ExportUtils.exportRealm(session, realm, true)); + } + + JsonSerialization.prettyMapper.writeValue(outStream, reps); + } + + @Override + public FileConnectionProvider create(KeycloakSession session) { + synchronized (allProviders) { + FileConnectionProvider fcProvider = allProviders.get(session); + if (fcProvider == null) { + InMemoryModel model = new InMemoryModel(); + fcProvider = new DefaultFileConnectionProvider(this, session, model); + allProviders.put(session, fcProvider); + session.getTransaction().enlist(fcProvider); + readModelFile(session); + //logger.info("Added session " + session.hashCode() + " total sessions=" + allModels.size()); + } + + return fcProvider; + } + } + + // commitCount is used for debugging. This allows you to easily run a test + // to a particular point and then examine the JSON file. + //private static int commitCount = 0; + void commit(KeycloakSession session) { + //commitCount++; + synchronized (allProviders) { + // in case commit was somehow called twice on the same session + if (!allProviders.containsKey(session)) return; + + try { + writeModelFile(session); + } finally { + allProviders.remove(session); + //logger.info("Removed session " + session.hashCode()); + //logger.info("*** commitCount=" + commitCount); + //logger.info("commit(): Session count=" + allModels.size()); + } + + // if (commitCount == 16) {Thread.dumpStack();System.exit(0);} + } + } + + void rollback(KeycloakSession session) { + synchronized (allProviders) { + allProviders.remove(session); + //logger.info("rollback(): Session count=" + allModels.size()); + } + } + + boolean isActive(KeycloakSession session) { + synchronized (allProviders) { + return allProviders.containsKey(session); + } + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "default"; + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + +} diff --git a/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProvider.java b/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProvider.java new file mode 100644 index 0000000000..a3ecfeff59 --- /dev/null +++ b/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.keycloak.connections.file; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakTransaction; +import org.keycloak.provider.Provider; + +/** + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public interface FileConnectionProvider extends Provider, KeycloakTransaction { + + InMemoryModel getModel(); + + void sessionClosed(KeycloakSession session); +} diff --git a/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProviderFactory.java b/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProviderFactory.java new file mode 100644 index 0000000000..92d161a9c8 --- /dev/null +++ b/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionProviderFactory.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.keycloak.connections.file; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public interface FileConnectionProviderFactory extends ProviderFactory { +} diff --git a/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionSpi.java b/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionSpi.java new file mode 100644 index 0000000000..4558be3059 --- /dev/null +++ b/connections/file/src/main/java/org/keycloak/connections/file/FileConnectionSpi.java @@ -0,0 +1,27 @@ +package org.keycloak.connections.file; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class FileConnectionSpi implements Spi { + + @Override + public String getName() { + return "connectionsFile"; + } + + @Override + public Class getProviderClass() { + return FileConnectionProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return FileConnectionProviderFactory.class; + } + +} diff --git a/connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java b/connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java new file mode 100644 index 0000000000..8fddce65a4 --- /dev/null +++ b/connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.keycloak.connections.file; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +/** + * This class provides an in-memory copy of the entire model for each + * Keycloak session. At the start of the session, the model is read + * from JSON. When the session's transaction ends, the model is written back + * out. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class InMemoryModel { + private final Map allRealms = new HashMap(); + + // realmId, userId, userModel + private final Map> allUsers = new HashMap>(); + + public InMemoryModel() { + } + + public void putRealm(String id, RealmModel realm) { + allRealms.put(id, realm); + allUsers.put(id, new HashMap()); + } + + public RealmModel getRealm(String id) { + return allRealms.get(id); + } + + public Collection getRealms() { + return allRealms.values(); + } + + public RealmModel getRealmByName(String name) { + for (RealmModel realm : getRealms()) { + if (realm.getName().equals(name)) return realm; + } + + return null; + } + + public boolean removeRealm(String id) { + allUsers.remove(id); + return (allRealms.remove(id) != null); + } + + protected Map realmUsers(String realmId) { + Map realmUsers = allUsers.get(realmId); + if (realmUsers == null) throw new NullPointerException("Realm users not found for id=" + realmId); + return realmUsers; + } + + public void putUser(String realmId, String userId, UserModel user) { + realmUsers(realmId).put(userId, user); + } + + public UserModel getUser(String realmId, String userId) { + return realmUsers(realmId).get(userId); + } + + public boolean hasUserWithUsername(String realmId, String username) { + for (UserModel user : getUsers(realmId)) { + if (user.getUsername().equals(username)) return true; + } + + return false; + } + + public Collection getUsers(String realmId) { + return realmUsers(realmId).values(); + } + + public boolean removeUser(String realmId, String userId) { + return (realmUsers(realmId).remove(userId) != null); + } + +} diff --git a/connections/file/src/main/resources/META-INF/services/org.keycloak.connections.file.FileConnectionProviderFactory b/connections/file/src/main/resources/META-INF/services/org.keycloak.connections.file.FileConnectionProviderFactory new file mode 100644 index 0000000000..d46ba7fb24 --- /dev/null +++ b/connections/file/src/main/resources/META-INF/services/org.keycloak.connections.file.FileConnectionProviderFactory @@ -0,0 +1 @@ +org.keycloak.connections.file.DefaultFileConnectionProviderFactory \ No newline at end of file diff --git a/connections/file/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/connections/file/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..b0ddd93218 --- /dev/null +++ b/connections/file/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.connections.file.FileConnectionSpi \ No newline at end of file diff --git a/connections/pom.xml b/connections/pom.xml index 21a5d63176..58918f57b8 100755 --- a/connections/pom.xml +++ b/connections/pom.xml @@ -17,6 +17,7 @@ jpa-liquibase infinispan mongo + file diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml index 43f9337ebb..52b97ef39e 100755 --- a/distribution/modules/build.xml +++ b/distribution/modules/build.xml @@ -172,6 +172,10 @@ + + + + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-file/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-file/main/module.xml new file mode 100644 index 0000000000..14c8bea489 --- /dev/null +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-file/main/module.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-model-file/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-model-file/main/module.xml index 7652c83689..e0ef89beba 100644 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-model-file/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-model-file/main/module.xml @@ -10,9 +10,7 @@ - - - + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml index c0e942c84d..2772582878 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml @@ -16,6 +16,7 @@ + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml index 8181ba8b87..341e44b9e3 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml @@ -16,6 +16,7 @@ + diff --git a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 1051a617fc..1da88be8ca 100755 --- a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -7,6 +7,7 @@ + diff --git a/model/file/pom.xml b/model/file/pom.xml index fa138c581f..a931f47710 100644 --- a/model/file/pom.xml +++ b/model/file/pom.xml @@ -35,6 +35,11 @@ keycloak-model-api ${project.version} + + org.keycloak + keycloak-connections-file + ${project.version} + org.codehaus.jackson jackson-mapper-asl diff --git a/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java index 4ef446ebca..52b3f39c95 100644 --- a/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java @@ -27,6 +27,8 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.utils.KeycloakModelUtils; import java.util.List; +import org.keycloak.connections.file.FileConnectionProvider; +import org.keycloak.connections.file.InMemoryModel; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.entities.RealmEntity; @@ -38,12 +40,19 @@ import org.keycloak.models.entities.RealmEntity; public class FileRealmProvider implements RealmProvider { private final KeycloakSession session; + private FileConnectionProvider fcProvider; private final InMemoryModel inMemoryModel; - public FileRealmProvider(KeycloakSession session, InMemoryModel inMemoryModel) { + public FileRealmProvider(KeycloakSession session, FileConnectionProvider fcProvider) { this.session = session; + this.fcProvider = fcProvider; session.enlistForClose(this); - this.inMemoryModel = inMemoryModel; + this.inMemoryModel = fcProvider.getModel(); + } + + @Override + public void close() { + fcProvider.sessionClosed(session); } @Override @@ -85,11 +94,6 @@ public class FileRealmProvider implements RealmProvider { return inMemoryModel.removeRealm(id); } - @Override - public void close() { - inMemoryModel.sessionClosed(session); - } - @Override public RoleModel getRoleById(String id, RealmModel realm) { return realm.getRoleById(id); diff --git a/model/file/src/main/java/org/keycloak/models/file/FileRealmProviderFactory.java b/model/file/src/main/java/org/keycloak/models/file/FileRealmProviderFactory.java index cf7135ee3e..211d8a5e64 100644 --- a/model/file/src/main/java/org/keycloak/models/file/FileRealmProviderFactory.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileRealmProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.models.file; import org.keycloak.Config; +import org.keycloak.connections.file.FileConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmProvider; @@ -30,19 +31,9 @@ import org.keycloak.models.RealmProviderFactory; */ public class FileRealmProviderFactory implements RealmProviderFactory { - private String directory; - private String fileName; - @Override public void init(Config.Scope config) { - this.fileName = config.get("fileName"); - if (fileName == null) fileName = "keycloak-model.json"; - InMemoryModel.setFileName(fileName); - this.directory = config.get("directory"); - if (directory == null) directory = System.getProperty("jboss.server.data.dir"); - if (directory == null) directory = "."; - InMemoryModel.setDirectory(directory); } @Override @@ -52,7 +43,8 @@ public class FileRealmProviderFactory implements RealmProviderFactory { @Override public RealmProvider create(KeycloakSession session) { - return new FileRealmProvider(session, InMemoryModel.getModelForSession(session)); + FileConnectionProvider fcProvider = session.getProvider(FileConnectionProvider.class); + return new FileRealmProvider(session, fcProvider); } @Override diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index d7909bd3a5..456831fc8b 100644 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -34,6 +34,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; +import org.keycloak.connections.file.FileConnectionProvider; +import org.keycloak.connections.file.InMemoryModel; import org.keycloak.models.ApplicationModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.ModelDuplicateException; @@ -49,17 +51,19 @@ import org.keycloak.models.utils.CredentialValidation; public class FileUserProvider implements UserProvider { private final KeycloakSession session; + private FileConnectionProvider fcProvider; private final InMemoryModel inMemoryModel; - public FileUserProvider(KeycloakSession session, InMemoryModel inMemoryModel) { + public FileUserProvider(KeycloakSession session, FileConnectionProvider fcProvider) { this.session = session; + this.fcProvider = fcProvider; session.enlistForClose(this); - this.inMemoryModel = inMemoryModel; + this.inMemoryModel = fcProvider.getModel(); } @Override public void close() { - inMemoryModel.sessionClosed(session); + fcProvider.sessionClosed(session); } @Override diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProviderFactory.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProviderFactory.java index 95fac5a852..e7f3674229 100644 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProviderFactory.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.models.file; import org.keycloak.Config; +import org.keycloak.connections.file.FileConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserProvider; @@ -40,7 +41,8 @@ public class FileUserProviderFactory implements UserProviderFactory { @Override public UserProvider create(KeycloakSession session) { - return new FileUserProvider(session, InMemoryModel.getModelForSession(session)); + FileConnectionProvider fcProvider = session.getProvider(FileConnectionProvider.class); + return new FileUserProvider(session, fcProvider); } @Override diff --git a/model/file/src/main/java/org/keycloak/models/file/InMemoryModel.java b/model/file/src/main/java/org/keycloak/models/file/InMemoryModel.java deleted file mode 100644 index e9733a5fa9..0000000000 --- a/model/file/src/main/java/org/keycloak/models/file/InMemoryModel.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @author tags. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package org.keycloak.models.file; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import org.keycloak.models.file.adapter.RealmAdapter; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.jboss.logging.Logger; -import org.keycloak.exportimport.Strategy; -import org.keycloak.exportimport.util.ExportUtils; -import org.keycloak.exportimport.util.ImportUtils; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakTransaction; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.util.JsonSerialization; - -/** - * This class provides an in-memory copy of the entire model for each - * Keycloak session. At the start of the session, the model is read - * from JSON. When the session's transaction ends, the model is written back - * out. - * - * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. - */ -public class InMemoryModel implements KeycloakTransaction { - private static final Logger logger = Logger.getLogger(InMemoryModel.class); - - private static String directory; - private static String fileName; - private final static Map allModels = new HashMap(); - - private final KeycloakSession session; - private final Map allRealms = new HashMap(); - - // realmId, userId, userModel - private final Map> allUsers = new HashMap>(); - - private boolean isRollbackOnly = false; - - static void setFileName(String dataFileName) { - fileName = dataFileName; - } - - static void setDirectory(String dataDirectory) { - directory = dataDirectory; - } - - /** - * Static factory to retrieve the model assigned to the session. - * - * @param session The Keycloak session. - * @return The in-memory model that will be flushed when the session is over. - */ - static InMemoryModel getModelForSession(KeycloakSession session) { - - synchronized (allModels) { - InMemoryModel model = allModels.get(session); - if (model == null) { - model = new InMemoryModel(session); - allModels.put(session, model); - session.getTransaction().enlist(model); - model.readModelFile(); - //logger.info("Added session " + session.hashCode() + " total sessions=" + allModels.size()); - } - - return model; - } - } - - private InMemoryModel(KeycloakSession session) { - this.session = session; - } - - private void readModelFile() { - File kcdata = new File(directory, fileName); - if (!kcdata.exists()) return; - - FileInputStream fis = null; - try { - fis = new FileInputStream(kcdata); - ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING); - } catch (IOException ioe) { - logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe); - } finally { - //logger.info("Read model file for session=" + session.hashCode()); - try { - if (fis != null) fis.close(); - } catch (IOException e) { - logger.error("Failed to close output stream.", e); - } - } - } - - void writeModelFile() { - FileOutputStream outStream = null; - File keycloakModelFile = new File(directory, fileName); - try { - outStream = new FileOutputStream(keycloakModelFile); - exportModel(outStream); - } catch (IOException e) { - logger.error("Unable to write model file " + keycloakModelFile.getAbsolutePath(), e); - } finally { - //logger.info("Wrote model file for session=" + session.hashCode()); - try { - if (outStream != null) outStream.close(); - } catch (IOException e) { - logger.error("Failed to close output stream.", e); - } - } - } - - protected void exportModel(FileOutputStream outStream) throws IOException { - List realms = session.realms().getRealms(); - List reps = new ArrayList(); - for (RealmModel realm : realms) { - reps.add(ExportUtils.exportRealm(session, realm, true)); - } - - JsonSerialization.prettyMapper.writeValue(outStream, reps); - } - - public void putRealm(String id, RealmAdapter realm) { - allRealms.put(id, realm); - allUsers.put(id, new HashMap()); - } - - public RealmModel getRealm(String id) { - return allRealms.get(id); - } - - public Collection getRealms() { - return allRealms.values(); - } - - public RealmModel getRealmByName(String name) { - for (RealmModel realm : getRealms()) { - if (realm.getName().equals(name)) return realm; - } - - return null; - } - - public boolean removeRealm(String id) { - allUsers.remove(id); - return (allRealms.remove(id) != null); - } - - protected Map realmUsers(String realmId) { - Map realmUsers = allUsers.get(realmId); - if (realmUsers == null) throw new NullPointerException("Realm users not found for id=" + realmId); - return realmUsers; - } - - public void putUser(String realmId, String userId, UserModel user) { - realmUsers(realmId).put(userId, user); - } - - public UserModel getUser(String realmId, String userId) { - return realmUsers(realmId).get(userId); - } - - public boolean hasUserWithUsername(String realmId, String username) { - for (UserModel user : getUsers(realmId)) { - if (user.getUsername().equals(username)) return true; - } - - return false; - } - - public Collection getUsers(String realmId) { - return realmUsers(realmId).values(); - } - - public boolean removeUser(String realmId, String userId) { - return (realmUsers(realmId).remove(userId) != null); - } - - void sessionClosed(KeycloakSession session) { - synchronized (allModels) { - allModels.remove(session); - //logger.info("Removed session " + session.hashCode()); - //logger.info("sessionClosed: Session count=" + allModels.size()); - } - } - - @Override - public void begin() { - } - - // commitCount is used for debugging. This allows you to easily run a test - // to a particular point and then examine the JSON file. - //private static int commitCount = 0; - - @Override - public void commit() { - //commitCount++; - synchronized (allModels) { - // in case commit was somehow called twice on the same session - if (!allModels.containsKey(session)) return; - - try { - writeModelFile(); - } finally { - allModels.remove(session); - //logger.info("Removed session " + session.hashCode()); - //logger.info("*** commitCount=" + commitCount); - //logger.info("commit(): Session count=" + allModels.size()); - } - - // if (commitCount == 16) {Thread.dumpStack();System.exit(0);} - } - } - - @Override - public void rollback() { - synchronized (allModels) { - allModels.remove(session); - //logger.info("rollback(): Session count=" + allModels.size()); - } - } - - @Override - public void setRollbackOnly() { - isRollbackOnly = true; - } - - @Override - public boolean getRollbackOnly() { - return isRollbackOnly; - } - - @Override - public boolean isActive() { - synchronized (allModels) { - return allModels.containsKey(session); - } - } - -} diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/ApplicationAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/ApplicationAdapter.java index 813d11fe9e..6edb24ad90 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/ApplicationAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/ApplicationAdapter.java @@ -29,13 +29,13 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.keycloak.connections.file.InMemoryModel; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.OAuthClientModel; import org.keycloak.models.UserModel; import org.keycloak.models.entities.ApplicationEntity; import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.entities.RoleEntity; -import org.keycloak.models.file.InMemoryModel; import org.keycloak.models.utils.KeycloakModelUtils; /** diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index 99ab488fe4..202cb8e387 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -46,6 +46,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; +import org.keycloak.connections.file.InMemoryModel; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.UserModel; import org.keycloak.models.entities.ApplicationEntity; @@ -53,7 +54,6 @@ import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.entities.OAuthClientEntity; import org.keycloak.models.entities.RealmEntity; import org.keycloak.models.entities.RoleEntity; -import org.keycloak.models.file.InMemoryModel; /** * RealmModel for JSON persistence. @@ -769,8 +769,16 @@ public class RealmAdapter implements RealmModel { @Override public void addRequiredCredential(String type) { + if (type == null) throw new NullPointerException("Credential type can not be null"); + RequiredCredentialModel credentialModel = initRequiredCredentialModel(type); - addRequiredCredential(credentialModel, realm.getRequiredCredentials()); + + List requiredCredList = realm.getRequiredCredentials(); + for (RequiredCredentialEntity cred : requiredCredList) { + if (type.equals(cred.getType())) return; + } + + addRequiredCredential(credentialModel, requiredCredList); } protected void addRequiredCredential(RequiredCredentialModel credentialModel, List persistentCollection) { diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index e6d08c77a1..f1a7a28ac9 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -33,11 +33,11 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.keycloak.connections.file.InMemoryModel; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.entities.FederatedIdentityEntity; import org.keycloak.models.entities.RoleEntity; import org.keycloak.models.entities.UserEntity; -import org.keycloak.models.file.InMemoryModel; /** * UserModel for JSON persistence. diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index ac11fe35cf..f16db97fa0 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -421,6 +421,7 @@ **/broker/*** + **/CacheTest.java file