From e88af874ca6cb0dc189317a07dfe0b09ef4b0c47 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 2 Dec 2016 19:25:17 -0500 Subject: [PATCH 1/2] finish --- .../org/keycloak/common/util/EnvUtil.java | 4 +- examples/providers/user-storage-jpa/README.md | 3 +- .../main/resources/META-INF/persistence.xml | 2 +- .../providers/user-storage-simple/README.md | 17 +- .../BasePropertiesStorageFactory.java | 69 ---- .../ClasspathPropertiesStorageFactory.java | 72 ----- .../ClasspathPropertiesStorageProvider.java | 72 ----- .../FilePropertiesStorageFactory.java | 77 ----- .../FilePropertiesStorageProvider.java | 129 -------- .../PropertyFileUserStorageProvider.java} | 99 +++--- ...ropertyFileUserStorageProviderFactory.java | 69 ++++ .../PropertyFileUserStorageProvider.java | 299 ++++++++++++++++++ ...ropertyFileUserStorageProviderFactory.java | 99 ++++++ ...eycloak.storage.UserStorageProviderFactory | 4 +- .../src/main/resources/test2-users.properties | 20 -- ...test-users.properties => users.properties} | 0 .../role/RoleLDAPStorageMapper.java | 1 - .../org/keycloak/models/jpa/RealmAdapter.java | 1 + .../mongo/keycloak/adapters/RealmAdapter.java | 1 + .../keycloak/models/utils/ComponentUtil.java | 4 + .../keycloak/component/ComponentFactory.java | 11 +- .../keycloak/credential/CredentialInput.java | 2 + .../credential/CredentialInputValidator.java | 4 + .../org/keycloak/models/cache/UserCache.java | 7 + .../models/utils/UserModelDelegate.java | 2 + .../provider/ProviderConfigProperty.java | 57 +++- .../ProviderConfigurationBuilder.java | 60 ++++ .../keycloak/storage/UserStorageProvider.java | 37 ++- .../AbstractUserAdapterFederatedStorage.java | 57 +++- .../storage/user/ImportedUserValidation.java | 2 +- .../storage/user/UserLookupProvider.java | 3 + .../storage/user/UserQueryProvider.java | 43 ++- .../user/UserRegistrationProvider.java | 3 + .../keycloak/storage/UserStorageManager.java | 4 +- 34 files changed, 820 insertions(+), 514 deletions(-) delete mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageFactory.java delete mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageFactory.java delete mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageProvider.java delete mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageFactory.java delete mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageProvider.java rename examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/{federation/properties/BasePropertiesStorageProvider.java => userstorage/readonly/PropertyFileUserStorageProvider.java} (69%) create mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/readonly/PropertyFileUserStorageProviderFactory.java create mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java create mode 100755 examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProviderFactory.java delete mode 100755 examples/providers/user-storage-simple/src/main/resources/test2-users.properties rename examples/providers/user-storage-simple/src/main/resources/{test-users.properties => users.properties} (100%) rename {server-spi-private => server-spi}/src/main/java/org/keycloak/models/utils/UserModelDelegate.java (98%) diff --git a/common/src/main/java/org/keycloak/common/util/EnvUtil.java b/common/src/main/java/org/keycloak/common/util/EnvUtil.java index ff40c6fd13..b7b41c739f 100755 --- a/common/src/main/java/org/keycloak/common/util/EnvUtil.java +++ b/common/src/main/java/org/keycloak/common/util/EnvUtil.java @@ -21,6 +21,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** + * Replaces any ${} strings with their corresponding system property. + * * @author Bill Burke * @version $Revision: 1 $ */ @@ -32,7 +34,7 @@ public final class EnvUtil { } /** - * Replaces any ${} strings with their corresponding environent variable. + * Replaces any ${} strings with their corresponding system property. * * @param val * @return diff --git a/examples/providers/user-storage-jpa/README.md b/examples/providers/user-storage-jpa/README.md index f965ef254a..89eacad397 100755 --- a/examples/providers/user-storage-jpa/README.md +++ b/examples/providers/user-storage-jpa/README.md @@ -10,4 +10,5 @@ Login and go to the User Federation tab and you should now see your deployed pro Add the provider, save it, then any new user you create will be stored and in the custom store you implemented. You can modify the example and hot deploy it using the above maven command again. -This example uses the built in in-memory datasource that comes with keycloak: ExampleDS. +This example uses the built in in-memory datasource that comes with keycloak: ExampleDS. NOTE!!! You'll have +to change this to be an xa datasource for this to work. diff --git a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml index 51082e159c..87b5028e32 100644 --- a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml +++ b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml @@ -5,7 +5,7 @@ http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"> - java:jboss/datasources/ExampleDS + java:jboss/datasources/KeycloakDS org.keycloak.examples.storage.user.UserEntity diff --git a/examples/providers/user-storage-simple/README.md b/examples/providers/user-storage-simple/README.md index 6549f8ed05..f194224fc3 100755 --- a/examples/providers/user-storage-simple/README.md +++ b/examples/providers/user-storage-simple/README.md @@ -1,4 +1,4 @@ -Example User Federation Provider +Example User Storage Provider =================================================== This is an example of user storage backed by a simple properties file. This properties file only contains username/password @@ -7,13 +7,10 @@ key pairs. To deploy this provider you must have Keycloak running in standalone mvn clean install wildfly:deploy +The "readonly-property-file" provider is hardcoded to look within the users.properties file embeded in the deployment jar +THere is one user 'tbrady' with a password of 'superbowl' -The ClasspathPropertiesStorageProvider is an example of a readonly provider. If you go to the Users/Federation - page of the admin console you will see this provider listed under "classpath-properties. To configure this provider you -specify a classpath to a properties file in the "path" field of the admin page for this plugin. This example includes -a "test-users.properties" within the JAR that you can use as the variable. - -The FilePropertiesStorageProvider is an example of a writable provider. It synchronizes changes made to -username and password with the properties file. If you go to the Users/Federation page of the admin console you will -see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to -a properties file in the "path" field of the admin page for this plugin. +The "writeable-property-file" provider can be configured to point to a property file on disk. It uses federated +storage to augment the property file with any other information the user wants. + +Our developer guide walks through the implementation of both of these providers. diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageFactory.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageFactory.java deleted file mode 100755 index c5bacdd0f9..0000000000 --- a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageFactory.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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.examples.federation.properties; - -import org.keycloak.component.ComponentModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.storage.UserStorageProviderFactory; -import org.keycloak.storage.UserStorageProviderModel; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class BasePropertiesStorageFactory implements UserStorageProviderFactory { - protected ConcurrentHashMap files = new ConcurrentHashMap(); - - - @Override - public T create(KeycloakSession session, ComponentModel model) { - // first get the path to our properties file from the stored configuration of this provider instance. - String path = model.getConfig().getFirst("path"); - if (path == null) { - throw new IllegalStateException("Path attribute not configured for provider"); - } - // see if we already loaded the config file - Properties props = files.get(path); - if (props != null) return (T)createProvider(session, new UserStorageProviderModel(model), props); - - - props = new Properties(); - InputStream is = getPropertiesFileStream(path); - if (is != null) { - try { - props.load(is); - is.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - // remember the properties file for next time - files.put(path, props); - return (T)createProvider(session, new UserStorageProviderModel(model), props); - } - - protected abstract InputStream getPropertiesFileStream(String path); - - protected abstract BasePropertiesStorageProvider createProvider(KeycloakSession session, UserStorageProviderModel model, Properties props); - -} diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageFactory.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageFactory.java deleted file mode 100755 index 2c0bb946d7..0000000000 --- a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageFactory.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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.examples.federation.properties; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; -import org.keycloak.storage.UserStorageProviderModel; - -import java.io.InputStream; -import java.util.List; -import java.util.Properties; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ClasspathPropertiesStorageFactory extends BasePropertiesStorageFactory { - - public static final String PROVIDER_NAME = "classpath-properties"; - protected static final List configProperties; - - static { - configProperties = ProviderConfigurationBuilder.create() - .property().name("path") - .type(ProviderConfigProperty.STRING_TYPE) - .label("Classpath") - .helpText("Classpath of properties file") - .add().build(); - } - - @Override - public List getConfigProperties() { - return configProperties; - } - - @Override - protected BasePropertiesStorageProvider createProvider(KeycloakSession session, UserStorageProviderModel model, Properties props) { - return new ClasspathPropertiesStorageProvider(session, model, props); - } - - protected InputStream getPropertiesFileStream(String path) { - InputStream is = getClass().getClassLoader().getResourceAsStream(path); - if (is == null) { - throw new IllegalStateException("Path not found for properties file"); - - } - return is; - } - - - - @Override - public String getId() { - return PROVIDER_NAME; - } -} diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageProvider.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageProvider.java deleted file mode 100755 index cf33b869e6..0000000000 --- a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageProvider.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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.examples.federation.properties; - -import org.keycloak.credential.CredentialInput; -import org.keycloak.credential.CredentialInputUpdater; -import org.keycloak.credential.CredentialModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.storage.ReadOnlyException; -import org.keycloak.storage.UserStorageProviderModel; -import org.keycloak.storage.adapter.AbstractUserAdapter; - -import java.util.Collections; -import java.util.Properties; -import java.util.Set; - -/** - * Creates a read-only user. Implements CredentialInputUpdater so that passwords cannot be updated - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class ClasspathPropertiesStorageProvider extends BasePropertiesStorageProvider implements CredentialInputUpdater { - - public ClasspathPropertiesStorageProvider(KeycloakSession session, UserStorageProviderModel model, Properties properties) { - super(session, model, properties); - } - - @Override - protected UserModel createAdapter(RealmModel realm, String username) { - return new AbstractUserAdapter(session, realm, model) { - @Override - public String getUsername() { - return username; - } - }; - } - - @Override - public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { - if (input.getType().equals(CredentialModel.PASSWORD)) throw new ReadOnlyException("user is read only for this update"); - - return false; - } - - @Override - public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { - - } - - @Override - public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) { - return Collections.EMPTY_SET; - } -} diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageFactory.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageFactory.java deleted file mode 100755 index f1cad0c5f3..0000000000 --- a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageFactory.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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.examples.federation.properties; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; -import org.keycloak.storage.UserStorageProviderModel; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.List; -import java.util.Properties; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class FilePropertiesStorageFactory extends BasePropertiesStorageFactory { - - public static final String PROVIDER_NAME = "file-properties"; - protected static final List configProperties; - - static { - configProperties = ProviderConfigurationBuilder.create() - .property().name("path") - .type(ProviderConfigProperty.STRING_TYPE) - .label("Path") - .helpText("File path to properties file") - .add().build(); - } - - @Override - public List getConfigProperties() { - return configProperties; - } - - @Override - protected BasePropertiesStorageProvider createProvider(KeycloakSession session, UserStorageProviderModel model, Properties props) { - return new FilePropertiesStorageProvider(session, props, model); - } - protected InputStream getPropertiesFileStream(String path) { - try { - return new FileInputStream(path); - } catch (FileNotFoundException e) { - return null; - } - } - - - /** - * Name of the provider. This will show up under the "Add Provider" select box on the Federation page in the - * admin console - * - * @return - */ - @Override - public String getId() { - return PROVIDER_NAME; - } -} diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageProvider.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageProvider.java deleted file mode 100755 index 666928c124..0000000000 --- a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageProvider.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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.examples.federation.properties; - -import org.keycloak.credential.CredentialInput; -import org.keycloak.credential.CredentialInputUpdater; -import org.keycloak.credential.CredentialModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; -import org.keycloak.storage.ReadOnlyException; -import org.keycloak.storage.UserStorageProviderModel; -import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; -import org.keycloak.storage.user.UserRegistrationProvider; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.HashSet; -import java.util.Properties; -import java.util.Set; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class FilePropertiesStorageProvider extends BasePropertiesStorageProvider implements - UserRegistrationProvider, - CredentialInputUpdater -{ - - public FilePropertiesStorageProvider(KeycloakSession session, Properties properties, UserStorageProviderModel model) { - super(session, model, properties); - } - - public void save() { - String path = model.getConfig().getFirst("path"); - try { - FileOutputStream fos = new FileOutputStream(path); - properties.store(fos, ""); - fos.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean removeUser(RealmModel realm, UserModel user) { - synchronized (properties) { - if (properties.remove(user.getUsername()) == null) return false; - save(); - return true; - } - } - - @Override - protected UserModel createAdapter(RealmModel realm, String username) { - return new AbstractUserAdapterFederatedStorage(session, realm, model) { - @Override - public String getUsername() { - return username; - } - - @Override - public void setUsername(String username) { - throw new ReadOnlyException("Cannot edit username"); - } - }; - } - - @Override - public UserModel addUser(RealmModel realm, String username) { - synchronized (properties) { - properties.setProperty(username, UNSET_PASSWORD); - save(); - } - return createAdapter(realm, username); - } - - @Override - public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { - if (!(input instanceof UserCredentialModel)) return false; - UserCredentialModel cred = (UserCredentialModel)input; - if (!cred.getType().equals(CredentialModel.PASSWORD)) return false; - synchronized (properties) { - properties.setProperty(user.getUsername(), cred.getValue()); - save(); - } - return true; - } - - @Override - public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { - if (!credentialType.equals(CredentialModel.PASSWORD)) return; - synchronized (properties) { - properties.setProperty(user.getUsername(), UNSET_PASSWORD); - save(); - } - - } - - private static final Set disableableTypes = new HashSet<>(); - - static { - disableableTypes.add(CredentialModel.PASSWORD); - } - - @Override - public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) { - - return disableableTypes; - } -} diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageProvider.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/readonly/PropertyFileUserStorageProvider.java similarity index 69% rename from examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageProvider.java rename to examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/readonly/PropertyFileUserStorageProvider.java index 6a18864ebe..e0961d865a 100755 --- a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageProvider.java +++ b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/readonly/PropertyFileUserStorageProvider.java @@ -15,56 +15,52 @@ * limitations under the License. */ -package org.keycloak.examples.federation.properties; +package org.keycloak.examples.userstorage.readonly; +import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputUpdater; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.CredentialModel; -import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; +import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; -import org.keycloak.storage.UserStorageProviderModel; +import org.keycloak.storage.adapter.AbstractUserAdapter; import org.keycloak.storage.user.UserLookupProvider; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import java.util.Set; /** * @author Bill Burke * @version $Revision: 1 $ */ -public abstract class BasePropertiesStorageProvider implements +public class PropertyFileUserStorageProvider implements UserStorageProvider, UserLookupProvider, - CredentialInputValidator + CredentialInputValidator, + CredentialInputUpdater { - public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD"; - protected KeycloakSession session; protected Properties properties; - protected UserStorageProviderModel model; + protected ComponentModel model; // map of loaded users in this transaction protected Map loadedUsers = new HashMap<>(); - public BasePropertiesStorageProvider(KeycloakSession session, UserStorageProviderModel model, Properties properties) { + public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) { this.session = session; this.model = model; this.properties = properties; } - public KeycloakSession getSession() { - return session; - } - - public Properties getProperties() { - return properties; - } + // UserLookupProvider methods @Override public UserModel getUserByUsername(String username, RealmModel realm) { @@ -79,6 +75,15 @@ public abstract class BasePropertiesStorageProvider implements return adapter; } + protected UserModel createAdapter(RealmModel realm, String username) { + return new AbstractUserAdapter(session, realm, model) { + @Override + public String getUsername() { + return username; + } + }; + } + @Override public UserModel getUserById(String id, RealmModel realm) { StorageId storageId = new StorageId(id); @@ -86,44 +91,18 @@ public abstract class BasePropertiesStorageProvider implements return getUserByUsername(username, realm); } - protected abstract UserModel createAdapter(RealmModel realm, String username); - @Override public UserModel getUserByEmail(String email, RealmModel realm) { return null; } - @Override - public void preRemove(RealmModel realm) { - // complete We don't care about the realm being removed - } - @Override - public void preRemove(RealmModel realm, RoleModel role) { - // complete we dont'care if a role is removed - - } - - @Override - public void preRemove(RealmModel realm, GroupModel group) { - // complete we dont'care if a role is removed - - } - - @Override - public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { - if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; - - UserCredentialModel cred = (UserCredentialModel)input; - String password = properties.getProperty(user.getUsername()); - if (password == null || password.equals(UNSET_PASSWORD)) return false; - return password.equals(cred.getValue()); - } + // CredentialInputValidator methods @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { String password = properties.getProperty(user.getUsername()); - return credentialType.equals(CredentialModel.PASSWORD) && password != null && !password.equals(UNSET_PASSWORD); + return credentialType.equals(CredentialModel.PASSWORD) && password != null; } @Override @@ -131,6 +110,36 @@ public abstract class BasePropertiesStorageProvider implements return credentialType.equals(CredentialModel.PASSWORD); } + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; + + UserCredentialModel cred = (UserCredentialModel)input; + String password = properties.getProperty(user.getUsername()); + if (password == null) return false; + return password.equals(cred.getValue()); + } + + // CredentialInputUpdater methods + + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (input.getType().equals(CredentialModel.PASSWORD)) throw new ReadOnlyException("user is read only for this update"); + + return false; + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + + } + + @Override + public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) { + return Collections.EMPTY_SET; + } + + @Override public void close() { diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/readonly/PropertyFileUserStorageProviderFactory.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/readonly/PropertyFileUserStorageProviderFactory.java new file mode 100755 index 0000000000..b0485bbf9e --- /dev/null +++ b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/readonly/PropertyFileUserStorageProviderFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.examples.userstorage.readonly; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.storage.UserStorageProviderFactory; +import org.keycloak.storage.UserStorageProviderModel; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PropertyFileUserStorageProviderFactory implements UserStorageProviderFactory { + + private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class); + + public static final String PROVIDER_NAME = "readonly-property-file"; + + protected Properties properties = new Properties(); + + @Override + public String getId() { + return PROVIDER_NAME; + } + + + @Override + public void init(Config.Scope config) { + InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties"); + + if (is == null) { + logger.warn("Could not find users.properties in classpath"); + } else { + try { + properties.load(is); + } catch (IOException ex) { + logger.error("Failed to load users.properties file", ex); + } + } + } + + @Override + public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) { + return new PropertyFileUserStorageProvider(session, model, properties); + } + +} diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java new file mode 100755 index 0000000000..1b256c9b53 --- /dev/null +++ b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java @@ -0,0 +1,299 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.examples.userstorage.writeable; + +import org.keycloak.common.util.EnvUtil; +import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputUpdater; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.credential.CredentialModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; +import org.keycloak.storage.user.UserLookupProvider; +import org.keycloak.storage.user.UserQueryProvider; +import org.keycloak.storage.user.UserRegistrationProvider; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PropertyFileUserStorageProvider implements + UserStorageProvider, + UserLookupProvider, + CredentialInputValidator, + CredentialInputUpdater, + UserRegistrationProvider, + UserQueryProvider { + + + public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD"; + + protected KeycloakSession session; + protected Properties properties; + protected ComponentModel model; + // map of loaded users in this transaction + protected Map loadedUsers = new HashMap<>(); + + public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) { + this.session = session; + this.model = model; + this.properties = properties; + } + + // UserLookupProvider methods + + @Override + public UserModel getUserByUsername(String username, RealmModel realm) { + UserModel adapter = loadedUsers.get(username); + if (adapter == null) { + String password = properties.getProperty(username); + if (password != null) { + adapter = createAdapter(realm, username); + loadedUsers.put(username, adapter); + } + } + return adapter; + } + + protected UserModel createAdapter(RealmModel realm, String username) { + return new AbstractUserAdapterFederatedStorage(session, realm, model) { + @Override + public String getUsername() { + return username; + } + + @Override + public void setUsername(String username) { + String pw = (String)properties.remove(username); + if (pw != null) { + properties.put(username, pw); + save(); + } + } + }; + } + + @Override + public UserModel getUserById(String id, RealmModel realm) { + StorageId storageId = new StorageId(id); + String username = storageId.getExternalId(); + return getUserByUsername(username, realm); + } + + @Override + public UserModel getUserByEmail(String email, RealmModel realm) { + return null; + } + + // UserQueryProvider methods + + @Override + public int getUsersCount(RealmModel realm) { + return properties.size(); + } + + @Override + public List getUsers(RealmModel realm) { + return getUsers(realm, 0, Integer.MAX_VALUE); + } + + @Override + public List getUsers(RealmModel realm, int firstResult, int maxResults) { + List users = new LinkedList<>(); + int i = 0; + for (Object obj : properties.keySet()) { + if (i++ < firstResult) continue; + String username = (String)obj; + UserModel user = getUserByUsername(username, realm); + users.add(user); + if (users.size() >= maxResults) break; + } + return users; + } + + // UserQueryProvider method implementations + + @Override + public List searchForUser(String search, RealmModel realm) { + return searchForUser(search, realm, 0, Integer.MAX_VALUE); + } + + @Override + public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { + List users = new LinkedList<>(); + int i = 0; + for (Object obj : properties.keySet()) { + String username = (String)obj; + if (!username.contains(search)) continue; + if (i++ < firstResult) continue; + UserModel user = getUserByUsername(username, realm); + users.add(user); + if (users.size() >= maxResults) break; + } + return users; + } + + @Override + public List searchForUser(Map params, RealmModel realm) { + return searchForUser(params, realm, 0, Integer.MAX_VALUE); + } + + @Override + public List searchForUser(Map params, RealmModel realm, int firstResult, int maxResults) { + // only support searching by username + String usernameSearchString = params.get("username"); + if (usernameSearchString == null) return Collections.EMPTY_LIST; + return searchForUser(usernameSearchString, realm, firstResult, maxResults); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + // runtime automatically handles querying UserFederatedStorage + return Collections.EMPTY_LIST; + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + // runtime automatically handles querying UserFederatedStorage + return Collections.EMPTY_LIST; + } + + @Override + public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { + // runtime automatically handles querying UserFederatedStorage + return Collections.EMPTY_LIST; + } + + + // UserRegistrationProvider method implementations + + public void save() { + String path = model.getConfig().getFirst("path"); + path = EnvUtil.replace(path); + try { + FileOutputStream fos = new FileOutputStream(path); + properties.store(fos, ""); + fos.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public UserModel addUser(RealmModel realm, String username) { + synchronized (properties) { + properties.setProperty(username, UNSET_PASSWORD); + save(); + } + return createAdapter(realm, username); + } + + @Override + public boolean removeUser(RealmModel realm, UserModel user) { + synchronized (properties) { + if (properties.remove(user.getUsername()) == null) return false; + save(); + return true; + } + } + + + + + + // CredentialInputValidator methods + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + String password = properties.getProperty(user.getUsername()); + return credentialType.equals(CredentialModel.PASSWORD) && password != null; + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(CredentialModel.PASSWORD); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false; + + UserCredentialModel cred = (UserCredentialModel)input; + String password = properties.getProperty(user.getUsername()); + if (password == null || UNSET_PASSWORD.equals(password)) return false; + return password.equals(cred.getValue()); + } + + // CredentialInputUpdater methods + + @Override + public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel)) return false; + if (!input.getType().equals(CredentialModel.PASSWORD)) return false; + UserCredentialModel cred = (UserCredentialModel)input; + synchronized (properties) { + properties.setProperty(user.getUsername(), cred.getValue()); + save(); + } + return true; + } + + @Override + public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) { + if (!credentialType.equals(CredentialModel.PASSWORD)) return; + synchronized (properties) { + properties.setProperty(user.getUsername(), UNSET_PASSWORD); + save(); + } + + } + + private static final Set disableableTypes = new HashSet<>(); + + static { + disableableTypes.add(CredentialModel.PASSWORD); + } + + @Override + public Set getDisableableCredentialTypes(RealmModel realm, UserModel user) { + + return disableableTypes; + } + @Override + public void close() { + + } +} diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProviderFactory.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProviderFactory.java new file mode 100755 index 0000000000..8c79b9af44 --- /dev/null +++ b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProviderFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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.examples.userstorage.writeable; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.common.util.EnvUtil; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.ComponentValidationException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.storage.UserStorageProviderFactory; +import org.keycloak.storage.UserStorageProviderModel; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PropertyFileUserStorageProviderFactory implements UserStorageProviderFactory { + + private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class); + + public static final String PROVIDER_NAME = "writeable-property-file"; + + protected static final List configMetadata; + + static { + configMetadata = ProviderConfigurationBuilder.create() + .property().name("path") + .type(ProviderConfigProperty.STRING_TYPE) + .label("Path") + .defaultValue("${jboss.server.config.dir}/example-users.properties") + .helpText("File path to properties file") + .add().build(); + } + + @Override + public List getConfigProperties() { + return configMetadata; + } + + @Override + public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException { + String fp = config.getConfig().getFirst("path"); + if (fp == null) throw new ComponentValidationException("user property file does not exist"); + fp = EnvUtil.replace(fp); + File file = new File(fp); + if (!file.exists()) { + throw new ComponentValidationException("user property file does not exist"); + } + } + + @Override + public String getId() { + return PROVIDER_NAME; + } + + @Override + public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) { + String path = model.getConfig().getFirst("path"); + path = EnvUtil.replace(path); + + Properties props = new Properties(); + try { + InputStream is = new FileInputStream(path); + props.load(is); + is.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return new PropertyFileUserStorageProvider(session, model, props); + } +} diff --git a/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory index f203c13655..fcb9380033 100644 --- a/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory +++ b/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -1,2 +1,2 @@ -org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory -org.keycloak.examples.federation.properties.FilePropertiesStorageFactory \ No newline at end of file +org.keycloak.examples.userstorage.readonly.PropertyFileUserStorageProviderFactory +org.keycloak.examples.userstorage.writeable.PropertyFileUserStorageProviderFactory \ No newline at end of file diff --git a/examples/providers/user-storage-simple/src/main/resources/test2-users.properties b/examples/providers/user-storage-simple/src/main/resources/test2-users.properties deleted file mode 100755 index e27d3ab3d6..0000000000 --- a/examples/providers/user-storage-simple/src/main/resources/test2-users.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright 2016 Red Hat, Inc. and/or its affiliates -# and other contributors as indicated by the @author tags. -# -# 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. -# - -# -#Sun Aug 03 10:52:57 EDT 2014 -belichick=superbowl diff --git a/examples/providers/user-storage-simple/src/main/resources/test-users.properties b/examples/providers/user-storage-simple/src/main/resources/users.properties similarity index 100% rename from examples/providers/user-storage-simple/src/main/resources/test-users.properties rename to examples/providers/user-storage-simple/src/main/resources/users.properties diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java index 90f9b7236a..f98a4397f5 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java @@ -25,7 +25,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; -import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RoleUtils; import org.keycloak.models.utils.UserModelDelegate; import org.keycloak.storage.ldap.LDAPStorageProvider; 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 830da5fbe5..0615e55714 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 @@ -1810,6 +1810,7 @@ public class RealmAdapter implements RealmModel, JpaModel { em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate(); em.flush(); setConfig(component, c); + ComponentUtil.notifyCreated(session, this, component); } 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 429d389dac..3d9a70908e 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 @@ -1706,6 +1706,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme } } updateRealm(); + ComponentUtil.notifyCreated(session, this, model); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java index 5efbb1ce37..969eacbd8a 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java @@ -89,5 +89,9 @@ public class ComponentUtil { ComponentFactory factory = getComponentFactory(session, model); factory.onCreate(session, realm, model); } + public static void notifyUpdated(KeycloakSession session, RealmModel realm, ComponentModel model) { + ComponentFactory factory = getComponentFactory(session, model); + factory.onUpdate(session, realm, model); + } } diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java index d519286dcb..da2f029195 100644 --- a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java +++ b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java @@ -39,13 +39,22 @@ public interface ComponentFactory ex return null; } - void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException; + default + void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException + { + + } default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) { } + default + void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) { + + } + /** * These are config properties that are common across all implementation of this component type * diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java b/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java index 805fb25d68..f9838b4e9b 100644 --- a/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java +++ b/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java @@ -17,6 +17,8 @@ package org.keycloak.credential; /** + * + * * @author Bill Burke * @version $Revision: 1 $ */ diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java b/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java index 0e03bba039..33c823053e 100644 --- a/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java +++ b/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java @@ -22,6 +22,10 @@ import org.keycloak.models.UserModel; import java.util.List; /** + * Implentations of this interface can validate CredentialInput, i.e. verify a password. + * UserStorageProviders and CredentialProviders can implement this interface. + * + * * @author Bill Burke * @version $Revision: 1 $ */ diff --git a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java index 260b0bef17..ab7080817f 100755 --- a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java @@ -22,6 +22,8 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; /** + * All these methods effect an entire cluster of Keycloak instances. + * * @author Bill Burke * @version $Revision: 1 $ */ @@ -39,5 +41,10 @@ public interface UserCache extends UserProvider { * @param realm */ void evict(RealmModel realm); + + /** + * Clear cache entirely. + * + */ void clear(); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/server-spi/src/main/java/org/keycloak/models/utils/UserModelDelegate.java similarity index 98% rename from server-spi-private/src/main/java/org/keycloak/models/utils/UserModelDelegate.java rename to server-spi/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index a08e18a4e7..e0c970843b 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -27,6 +27,8 @@ import java.util.Map; import java.util.Set; /** + * Delegation pattern. Used to proxy UserModel implementations. + * * @author Bill Burke * @version $Revision: 1 $ */ diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java index c452e787a5..41f7430836 100755 --- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java +++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java @@ -21,24 +21,32 @@ import java.util.Arrays; import java.util.List; /** -* @author Bill Burke -* @version $Revision: 1 $ -*/ + * Configuration property metadata. Used to render generic configuration pages for Keycloak extensions in the admin console. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ public class ProviderConfigProperty { public static final String BOOLEAN_TYPE="boolean"; public static final String STRING_TYPE="String"; - // Possibility to configure multiple String values of any value (something like "redirect_uris" for clients) + /** + * Possibility to configure multiple String values of any value (something like "redirect_uris" for clients) + */ public static final String MULTIVALUED_STRING_TYPE="MultivaluedString"; public static final String SCRIPT_TYPE="Script"; public static final String FILE_TYPE="File"; public static final String ROLE_TYPE="Role"; - // Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select) + /** + * Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select) + */ public static final String LIST_TYPE="List"; - // Possibility to configure multiple String values, which needs to be chosen from the list of predefined values (HTML select with multiple) + /** + * Possibility to configure multiple String values, which needs to be chosen from the list of predefined values (HTML select with multiple) + */ public static final String MULTIVALUED_LIST_TYPE="MultivaluedList"; public static final String CLIENT_LIST_TYPE="ClientList"; @@ -77,6 +85,11 @@ public class ProviderConfigProperty { this.secret = secret; } + /** + * Name of the config variable stored in the database + * + * @return + */ public String getName() { return name; } @@ -85,6 +98,11 @@ public class ProviderConfigProperty { this.name = name; } + /** + * Label shown in the admin console when configuring the variable + * + * @return + */ public String getLabel() { return label; } @@ -93,6 +111,12 @@ public class ProviderConfigProperty { this.label = label; } + /** + * Type of the variable. i.e. boolean, string etc. See the constants declared in this class for what your choices + * are. + * + * @return + */ public String getType() { return type; } @@ -101,6 +125,11 @@ public class ProviderConfigProperty { this.type = type; } + /** + * Default value for the variable + * + * @return + */ public Object getDefaultValue() { return defaultValue; } @@ -109,6 +138,11 @@ public class ProviderConfigProperty { this.defaultValue = defaultValue; } + /** + * For list types, this is a list of choices to choose from. + * + * @return + */ public List getOptions() { return options; } @@ -117,6 +151,11 @@ public class ProviderConfigProperty { this.options = options; } + /** + * Help text that will be displayed in the admin console tooltip + * + * @return + */ public String getHelpText() { return helpText; } @@ -125,6 +164,12 @@ public class ProviderConfigProperty { this.helpText = helpText; } + /** + * If true, this variable is only writeable. It will never be viewable. This is important for things like + * passwords in which you never want to display them on the screen. + * + * @return + */ public boolean isSecret() { return secret; } diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java index 194e8e8fce..554eca10b0 100644 --- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java +++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java @@ -22,6 +22,9 @@ import java.util.LinkedList; import java.util.List; /** + * Builds a list of ProviderConfigProperty instances. + * + * * @author Stian Thorgersen */ public class ProviderConfigurationBuilder { @@ -58,6 +61,11 @@ public class ProviderConfigurationBuilder { return this; } + /** + * Create the list. + * + * @return + */ public List build() { return properties; } @@ -77,42 +85,94 @@ public class ProviderConfigurationBuilder { return this; } + /** + * Label that will be shown for this configuration property in the admin console + * + * @param label + * @return + */ public ProviderConfigPropertyBuilder label(String label) { this.label = label; return this; } + /** + * Help text that will be shown for this configuration property in the admin console + * when you hover over the tooltip + * + * @param helpText + * @return + */ public ProviderConfigPropertyBuilder helpText(String helpText) { this.helpText = helpText; return this; } + /** + * Property type. i.e. boolean, string. + * @see ProviderConfigProperty + * + * + * @param type + * @return + */ public ProviderConfigPropertyBuilder type(String type) { this.type = type; return this; } + /** + * Default value that will be shown when configuring this property for the first time + * + * @param defaultValue + * @return + */ public ProviderConfigPropertyBuilder defaultValue(Object defaultValue) { this.defaultValue = defaultValue; return this; } + /** + * If configuring a list type, these are the options you can choose from. + * + * @param options + * @return + */ public ProviderConfigPropertyBuilder options(String... options) { this.options = Arrays.asList(options); return this; } + /** + * If configuring a list type, these are the options you can choose from. + * + * @param options + * @return + */ public ProviderConfigPropertyBuilder options(List options) { this.options = options; return this; } + /** + * If turned on, this property is only writable and never readable. + * This is useful for things like passwords where you never want an admin + * to be able to see what the password is. + * + * @param secret + * @return + */ public ProviderConfigPropertyBuilder secret(boolean secret) { this.secret = secret; return this; } + /** + * Add the current property, and start building the next one + * + * @return + */ public ProviderConfigurationBuilder add() { ProviderConfigProperty property = new ProviderConfigProperty(); property.setName(name); diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java index 922c3493cf..92d7c1a5ce 100644 --- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java @@ -28,9 +28,40 @@ import org.keycloak.provider.Provider; public interface UserStorageProvider extends Provider { - void preRemove(RealmModel realm); - void preRemove(RealmModel realm, GroupModel group); - void preRemove(RealmModel realm, RoleModel role); + /** + * Callback when a realm is removed. Implement this if, for example, you want to do some + * cleanup in your user storage when a realm is removed + * + * @param realm + */ + default + void preRemove(RealmModel realm) { + + } + + /** + * Callback when a group is removed. Allows you to do things like remove a user + * group mapping in your external store if appropriate + * + * @param realm + * @param group + */ + default + void preRemove(RealmModel realm, GroupModel group) { + + } + + /** + * Callback when a role is removed. Allows you to do things like remove a user + * role mapping in your external store if appropriate + + * @param realm + * @param role + */ + default + void preRemove(RealmModel realm, RoleModel role) { + + } /** * Optional type that can be used by implementations to diff --git a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java index c93dac49b2..fe9a41ed1b 100644 --- a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java +++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java @@ -40,7 +40,7 @@ import java.util.Set; * of "f:" + providerId + ":" + getUsername(). UserModel properties like enabled, firstName, lastName, email, etc. are all * stored as attributes in federated storage. * - * isEnabled() defaults to true if the ENABLED_ATTRIBUTE isn't set in federated + * isEnabled() defaults to true if the ENABLED_ATTRIBUTE isn't set in federated storage * * @author Bill Burke * @version $Revision: 1 $ @@ -116,6 +116,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { return true; } + /** + * Gets groups from federated storage and automatically appends default groups of realm. + * Also calls getGroupsInternal() method + * to pull group membership from provider. Implementors can override that method + * + * + * @return + */ @Override public Set getGroups() { Set set = new HashSet<>(); @@ -143,6 +151,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { return RoleUtils.isMember(roles, group); } + /** + * Gets role mappings from federated storage and automatically appends default roles. + * Also calls getRoleMappingsInternal() method + * to pull role mappings from provider. Implementors can override that method + * + * + * @return + */ @Override public Set getRealmRoleMappings() { Set roleMappings = getRoleMappings(); @@ -157,6 +173,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { return realmRoles; } + /** + * Gets role mappings from federated storage and automatically appends default roles. + * Also calls getRoleMappingsInternal() method + * to pull role mappings from provider. Implementors can override that method + * + * + * @return + */ @Override public Set getClientRoleMappings(ClientModel app) { Set roleMappings = getRoleMappings(); @@ -202,6 +226,13 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { return Collections.EMPTY_SET; } + /** + * Gets role mappings from federated storage and automatically appends default roles. + * Also calls getRoleMappingsInternal() method + * to pull role mappings from provider. Implementors can override that method + * + * @return + */ @Override public Set getRoleMappings() { Set set = new HashSet<>(); @@ -343,6 +374,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { return getFirstAttribute(FIRST_NAME_ATTRIBUTE); } + /** + * Stores as attribute in federated storage. + * FIRST_NAME_ATTRIBUTE + * + * @param firstName + */ @Override public void setFirstName(String firstName) { setSingleAttribute(FIRST_NAME_ATTRIBUTE, firstName); @@ -354,6 +391,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { return getFirstAttribute(LAST_NAME_ATTRIBUTE); } + /** + * Stores as attribute in federated storage. + * LAST_NAME_ATTRIBUTE + * + * @param lastName + */ @Override public void setLastName(String lastName) { setSingleAttribute(LAST_NAME_ATTRIBUTE, lastName); @@ -365,6 +408,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { return getFirstAttribute(EMAIL_ATTRIBUTE); } + /** + * Stores as attribute in federated storage. + * EMAIL_ATTRIBUTE + * + * @param email + */ @Override public void setEmail(String email) { setSingleAttribute(EMAIL_ATTRIBUTE, email); @@ -378,6 +427,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel { else return Boolean.valueOf(val); } + /** + * Stores as attribute in federated storage. + * EMAIL_VERIFIED_ATTRIBUTE + * + * @param verified + */ @Override public void setEmailVerified(boolean verified) { setSingleAttribute(EMAIL_VERIFIED_ATTRIBUTE, Boolean.toString(verified)); diff --git a/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java b/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java index 0ceec664b8..7e374e39c0 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java @@ -28,7 +28,7 @@ import org.keycloak.models.UserModel; */ public interface ImportedUserValidation { /** - * If this method returns null, then the user storage in local storage will be removed + * If this method returns null, then the user in local storage will be removed * * @param realm * @param user diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java index 039986212f..ccbf46e1d1 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java @@ -20,6 +20,9 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** + * Optional capability interface implemented by UserStorageProviders. This interface is required + * if you want the UserStorageProvider to support basic login capabilities. + * * @author Bill Burke * @version $Revision: 1 $ */ diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java index 877b09f2db..eb179c93db 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java @@ -24,6 +24,10 @@ import java.util.List; import java.util.Map; /** + * Optional capability interface implemented by UserStorageProviders. + * Defines complex queries that are used to locate one or more users. You must implement this interface + * if you want to view and manager users from the administration console. + * * @author Bill Burke * @version $Revision: 1 $ */ @@ -99,10 +103,47 @@ public interface UserQueryProvider { */ List searchForUser(Map params, RealmModel realm, int firstResult, int maxResults); + /** + * Get users that belong to a specific group. Implementations do not have to search in UserFederatedStorageProvider + * as this is done automatically. + * + * @see org.keycloak.storage.federated.UserFederatedStorageProvider + * + * @param realm + * @param group + * @param firstResult + * @param maxResults + * @return + */ List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults); + + /** + * Get users that belong to a specific group. Implementations do not have to search in UserFederatedStorageProvider + * as this is done automatically. + * + * @see org.keycloak.storage.federated.UserFederatedStorageProvider + * + * + * + * @param realm + * @param group + * @return + */ List getGroupMembers(RealmModel realm, GroupModel group); + /** + * Search for users that have a specific attribute with a specific value. + * Implementations do not have to search in UserFederatedStorageProvider + * as this is done automatically. + * + * @see org.keycloak.storage.federated.UserFederatedStorageProvider + * - // Searching by UserModel.attribute (not property) + * + * @param attrName + * @param attrValue + * @param realm + * @return + */ List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm); } diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java index 25bb35e353..b3ec00d85a 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java @@ -21,6 +21,9 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; /** + * Optional capability interface implemented by UserStorageProviders. + * Implement this interface if your provider supports adding and removing users. + * * @author Bill Burke * @version $Revision: 1 $ */ diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 0e48c554d0..906e2252ad 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -285,7 +285,9 @@ public class UserStorageManager implements UserProvider, OnUserCache { protected List importValidation(RealmModel realm, List users) { List tmp = new LinkedList<>(); for (UserModel user : users) { - tmp.add(importValidation(realm, user)); + UserModel model = importValidation(realm, user); + if (model == null) continue; + tmp.add(model); } return tmp; } From 672e1b35759697e6dfaba6f3d7a2e8f0276aea6b Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 2 Dec 2016 20:14:01 -0500 Subject: [PATCH 2/2] oops --- .../jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java | 2 +- .../keycloak/models/mongo/keycloak/adapters/RealmAdapter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 0615e55714..1c4393d899 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 @@ -1810,7 +1810,7 @@ public class RealmAdapter implements RealmModel, JpaModel { em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate(); em.flush(); setConfig(component, c); - ComponentUtil.notifyCreated(session, this, component); + ComponentUtil.notifyUpdated(session, this, component); } 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 3d9a70908e..077296d3ed 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 @@ -1706,7 +1706,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme } } updateRealm(); - ComponentUtil.notifyCreated(session, this, model); + ComponentUtil.notifyUpdated(session, this, model); }