From c709598fdda635e65b20b826cdcb06933290d296 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 20 Jul 2016 10:59:45 -0400 Subject: [PATCH 1/2] user fed spi simple test --- .../keycloak/models/jpa/JpaUserProvider.java | 12 +- .../org/keycloak/models/UserProvider.java | 10 +- .../keycloak/models/utils/DefaultRoles.java | 51 +++ .../java/org/keycloak/storage/StorageId.java | 6 + .../org/keycloak/storage/StorageProvider.java | 1 - .../keycloak/storage/UserStorageManager.java | 10 +- .../storage/adapter/AbstractUserAdapter.java | 315 +++++++++++-- .../AbstractUserAdapterFederatedStorage.java | 419 ++++++++++++++++++ .../storage/federated/CredentialModel.java | 24 - .../UserCredentialAuthenticationProvider.java | 7 +- .../UserCredentialValidatorProvider.java | 7 +- .../user}/UserLookupProvider.java | 5 +- .../user}/UserQueryProvider.java | 6 +- .../user}/UserUpdateProvider.java | 6 +- .../storage/UserFederationStorageTest.java | 88 ++++ .../storage/UserPropertyFileStorage.java | 121 +++++ .../UserPropertyFileStorageFactory.java | 87 ++++ ...rg.keycloak.storage.StorageProviderFactory | 1 + .../src/test/resources/log4j.properties | 3 +- .../storage-test/user-password.properties | 1 + 20 files changed, 1097 insertions(+), 83 deletions(-) create mode 100644 server-spi/src/main/java/org/keycloak/models/utils/DefaultRoles.java create mode 100644 server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java delete mode 100644 server-spi/src/main/java/org/keycloak/storage/federated/CredentialModel.java rename server-spi/src/main/java/org/keycloak/{models => storage/user}/UserCredentialAuthenticationProvider.java (82%) rename server-spi/src/main/java/org/keycloak/{models => storage/user}/UserCredentialValidatorProvider.java (82%) rename server-spi/src/main/java/org/keycloak/{models => storage/user}/UserLookupProvider.java (89%) rename server-spi/src/main/java/org/keycloak/{models => storage/user}/UserQueryProvider.java (92%) rename server-spi/src/main/java/org/keycloak/{models => storage/user}/UserUpdateProvider.java (88%) create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java create mode 100644 testsuite/integration/src/test/resources/META-INF/services/org.keycloak.storage.StorageProviderFactory create mode 100644 testsuite/integration/src/test/resources/storage-test/user-password.properties diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 78024538b6..02403ee3aa 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -27,7 +27,6 @@ import org.keycloak.models.ModelException; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; -import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserCredentialModel; @@ -41,6 +40,7 @@ import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity; import org.keycloak.models.jpa.entities.UserConsentRoleEntity; import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.utils.CredentialValidation; +import org.keycloak.models.utils.DefaultRoles; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.storage.StorageProviderModel; @@ -88,15 +88,7 @@ public class JpaUserProvider implements UserProvider { UserAdapter userModel = new UserAdapter(session, realm, em, entity); if (addDefaultRoles) { - for (String r : realm.getDefaultRoles()) { - userModel.grantRoleImpl(realm.getRole(r)); // No need to check if user has role as it's new user - } - - for (ClientModel application : realm.getClients()) { - for (String r : application.getDefaultRoles()) { - userModel.grantRoleImpl(application.getRole(r)); // No need to check if user has role as it's new user - } - } + DefaultRoles.addDefaultRoles(realm, userModel); for (GroupModel g : realm.getDefaultGroups()) { userModel.joinGroupImpl(g); // No need to check if user has group as it's new user diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index d903799948..982453e092 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java @@ -19,6 +19,10 @@ package org.keycloak.models; import org.keycloak.provider.Provider; import org.keycloak.storage.StorageProviderModel; +import org.keycloak.storage.user.UserCredentialValidatorProvider; +import org.keycloak.storage.user.UserLookupProvider; +import org.keycloak.storage.user.UserQueryProvider; +import org.keycloak.storage.user.UserUpdateProvider; import java.util.List; import java.util.Set; @@ -27,7 +31,11 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public interface UserProvider extends Provider, UserLookupProvider, UserQueryProvider, UserCredentialValidatorProvider, UserUpdateProvider { +public interface UserProvider extends Provider, + UserLookupProvider, + UserQueryProvider, + UserCredentialValidatorProvider, + UserUpdateProvider { // Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink); diff --git a/server-spi/src/main/java/org/keycloak/models/utils/DefaultRoles.java b/server-spi/src/main/java/org/keycloak/models/utils/DefaultRoles.java new file mode 100644 index 0000000000..450c4eb162 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/utils/DefaultRoles.java @@ -0,0 +1,51 @@ +/* + * 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.models.utils; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class DefaultRoles { + public static Set getDefaultRoles(RealmModel realm) { + Set set = new HashSet<>(); + for (String r : realm.getDefaultRoles()) { + set.add(realm.getRole(r)); + } + + for (ClientModel application : realm.getClients()) { + for (String r : application.getDefaultRoles()) { + set.add(application.getRole(r)); + } + } + return set; + + } + public static void addDefaultRoles(RealmModel realm, UserModel userModel) { + for (RoleModel role : getDefaultRoles(realm)) { + userModel.grantRole(role); + } + } +} diff --git a/server-spi/src/main/java/org/keycloak/storage/StorageId.java b/server-spi/src/main/java/org/keycloak/storage/StorageId.java index 4ecac6eed8..56207403e3 100644 --- a/server-spi/src/main/java/org/keycloak/storage/StorageId.java +++ b/server-spi/src/main/java/org/keycloak/storage/StorageId.java @@ -42,6 +42,12 @@ public class StorageId implements Serializable { } + public StorageId(String providerId, String storageId) { + this.id = "f:" + providerId + ":" + storageId; + this.providerId = providerId; + this.storageId = storageId; + } + public static String resolveProviderId(UserModel user) { return new StorageId(user.getId()).getProviderId(); } diff --git a/server-spi/src/main/java/org/keycloak/storage/StorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/StorageProvider.java index 6746d5d8fa..56de3ab82c 100644 --- a/server-spi/src/main/java/org/keycloak/storage/StorageProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/StorageProvider.java @@ -29,7 +29,6 @@ import org.keycloak.provider.Provider; * @version $Revision: 1 $ */ public interface StorageProvider extends Provider { - StorageProviderModel getModel(); void preRemove(RealmModel realm); void preRemove(RealmModel realm, GroupModel group); diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java index f740f7689f..24f3c3d77b 100755 --- a/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -28,16 +28,16 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserConsentModel; -import org.keycloak.models.UserCredentialAuthenticationProvider; +import org.keycloak.storage.user.UserCredentialAuthenticationProvider; import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserCredentialValidatorProvider; +import org.keycloak.storage.user.UserCredentialValidatorProvider; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.UserLookupProvider; +import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; -import org.keycloak.models.UserQueryProvider; -import org.keycloak.models.UserUpdateProvider; +import org.keycloak.storage.user.UserQueryProvider; +import org.keycloak.storage.user.UserUpdateProvider; import org.keycloak.models.utils.CredentialValidation; import org.keycloak.storage.federated.UserFederatedStorageProvider; diff --git a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java index 114b0c34c3..1d3ff73b46 100644 --- a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java +++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java @@ -16,138 +16,211 @@ */ package org.keycloak.storage.adapter; +import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; -import org.keycloak.storage.federated.UserFederatedStorageProvider; +import org.keycloak.models.utils.DefaultRoles; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.StorageProviderModel; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** + * This abstract class provides implementations for everything but getUsername(). getId() returns a default value + * of "f:" + providerId + ":" + getUsername(). isEnabled() returns true. getRoleMappings() will return default roles. + * getGroups() will return default groups. + * + * All other read methods return null, an empty collection, or false depending + * on the type. All update methods throw a ReadOnlyException. + * + * Provider implementors should override the methods for attributes, properties, and mappings they support. + * * @author Bill Burke * @version $Revision: 1 $ */ public abstract class AbstractUserAdapter implements UserModel { + public static class ReadOnlyException extends RuntimeException { + public ReadOnlyException(String message) { + super(message); + } + } protected KeycloakSession session; protected RealmModel realm; + protected StorageProviderModel storageProviderModel; - public UserFederatedStorageProvider getFederatedStorage() { - return null; + public AbstractUserAdapter(KeycloakSession session, RealmModel realm, StorageProviderModel storageProviderModel) { + this.session = session; + this.realm = realm; + this.storageProviderModel = storageProviderModel; } @Override public Set getRequiredActions() { - return getFederatedStorage().getRequiredActions(realm, this); + return Collections.EMPTY_SET; } @Override public void addRequiredAction(String action) { - getFederatedStorage().addRequiredAction(realm, this, action); + throw new ReadOnlyException("user is read only for this update"); } @Override public void removeRequiredAction(String action) { - getFederatedStorage().removeRequiredAction(realm, this, action); + throw new ReadOnlyException("user is read only for this update"); } @Override public void addRequiredAction(RequiredAction action) { - getFederatedStorage().addRequiredAction(realm, this, action.name()); + throw new ReadOnlyException("user is read only for this update"); } @Override public void removeRequiredAction(RequiredAction action) { - getFederatedStorage().removeRequiredAction(realm, this, action.name()); + throw new ReadOnlyException("user is read only for this update"); + } + + /** + * Get group membership mappings that are managed by this storage provider + * + * @return + */ + protected Set getGroupsInternal() { + return Collections.EMPTY_SET; + } + + /** + * Should the realm's default groups be appended to getGroups() call? + * If your storage provider is not managing group mappings then it is recommended that + * this method return true + * + * @return + */ + protected boolean appendDefaultGroups() { + return true; } @Override public Set getGroups() { - return null; + Set set = new HashSet<>(); + if (appendDefaultGroups()) set.addAll(realm.getDefaultGroups()); + set.addAll(getGroupsInternal()); + return set; } @Override public void joinGroup(GroupModel group) { + throw new ReadOnlyException("user is read only for this update"); } @Override public void leaveGroup(GroupModel group) { + throw new ReadOnlyException("user is read only for this update"); } @Override public boolean isMemberOf(GroupModel group) { - return false; - } - - @Override - public String getFederationLink() { - return null; - } - - @Override - public void setFederationLink(String link) { - - } - - @Override - public String getServiceAccountClientLink() { - return null; - } - - @Override - public void setServiceAccountClientLink(String clientInternalId) { - + Set roles = getGroups(); + return KeycloakModelUtils.isMember(roles, group); } @Override public Set getRealmRoleMappings() { - return null; + Set roleMappings = getRoleMappings(); + + Set realmRoles = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof RealmModel) { + realmRoles.add(role); + } + } + return realmRoles; } @Override public Set getClientRoleMappings(ClientModel app) { - return null; + Set roleMappings = getRoleMappings(); + + Set roles = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof ClientModel) { + ClientModel appModel = (ClientModel) container; + if (appModel.getId().equals(app.getId())) { + roles.add(role); + } + } + } + return roles; } @Override public boolean hasRole(RoleModel role) { - return false; + Set roles = getRoleMappings(); + return KeycloakModelUtils.hasRole(roles, role); } @Override public void grantRole(RoleModel role) { + throw new ReadOnlyException("user is read only for this update"); } + /** + * Should the realm's default roles be appended to getRoleMappings() call? + * If your storage provider is not managing all role mappings then it is recommended that + * this method return true + * + * @return + */ + protected boolean appendDefaultRolesToRoleMappings() { + return true; + } + + protected Set getRoleMappingsInternal() { + return Collections.EMPTY_SET; + } + @Override public Set getRoleMappings() { - return null; + Set set = new HashSet<>(); + if (appendDefaultRolesToRoleMappings()) set.addAll(DefaultRoles.getDefaultRoles(realm)); + set.addAll(getRoleMappingsInternal()); + return set; } + @Override public void deleteRoleMapping(RoleModel role) { + throw new ReadOnlyException("user is read only for this update"); } @Override public boolean isEnabled() { - return false; + return true; } @Override public void setEnabled(boolean enabled) { - + throw new ReadOnlyException("user is read only for this update"); } @Override @@ -157,6 +230,176 @@ public abstract class AbstractUserAdapter implements UserModel { @Override public void setOtpEnabled(boolean totp) { + throw new ReadOnlyException("user is read only for this update"); + + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public String getFederationLink() { + return null; + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public void setFederationLink(String link) { + throw new ReadOnlyException("user is read only for this update"); + + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public String getServiceAccountClientLink() { + return null; + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public void setServiceAccountClientLink(String clientInternalId) { + throw new ReadOnlyException("user is read only for this update"); + + } + + protected StorageId storageId; + + /** + * Defaults to 'f:' + storageProvider.getId() + ':' + getUsername() + * + * @return + */ + @Override + public String getId() { + if (storageId == null) { + storageId = new StorageId(storageProviderModel.getId(), getUsername()); + } + return storageId.getId(); + } + + @Override + public void setUsername(String username) { + throw new ReadOnlyException("user is read only for this update"); + } + + protected long created = System.currentTimeMillis(); + + @Override + public Long getCreatedTimestamp() { + return created; + } + + @Override + public void setCreatedTimestamp(Long timestamp) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public void setSingleAttribute(String name, String value) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public void removeAttribute(String name) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public void setAttribute(String name, List values) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public String getFirstAttribute(String name) { + return null; + } + + @Override + public Map> getAttributes() { + return new MultivaluedHashMap<>(); + } + + @Override + public List getAttribute(String name) { + return null; + } + + @Override + public String getFirstName() { + return null; + } + + @Override + public void setFirstName(String firstName) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public String getLastName() { + return null; + } + + @Override + public void setLastName(String lastName) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public String getEmail() { + return null; + } + + @Override + public void setEmail(String email) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public boolean isEmailVerified() { + return false; + } + + @Override + public void setEmailVerified(boolean verified) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public void updateCredential(UserCredentialModel cred) { + throw new ReadOnlyException("user is read only for this update"); + + } + + @Override + public List getCredentialsDirectly() { + return Collections.EMPTY_LIST; + } + + @Override + public void updateCredentialDirectly(UserCredentialValueModel cred) { + throw new ReadOnlyException("user is read only for this update"); } } 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 new file mode 100644 index 0000000000..bcc2651cb2 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java @@ -0,0 +1,419 @@ +/* + * 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.storage.adapter; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleContainerModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.DefaultRoles; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.StorageProviderModel; +import org.keycloak.storage.federated.UserFederatedStorageProvider; + +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Assumes everything is managed by federated storage except for username. getId() returns a default value + * 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 + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public abstract class AbstractUserAdapterFederatedStorage implements UserModel { + public static String FIRST_NAME_ATTRIBUTE = "FIRST_NAME"; + public static String LAST_NAME_ATTRIBUTE = "LAST_NAME"; + public static String EMAIL_ATTRIBUTE = "EMAIL"; + public static String EMAIL_VERIFIED_ATTRIBUTE = "EMAIL_VERIFIED"; + public static String CREATED_TIMESTAMP_ATTRIBUTE = "CREATED_TIMESTAMP"; + public static String ENABLED_ATTRIBUTE = "ENABLED"; + public static String OTP_ENABLED_ATTRIBUTE = "OTP_ENABLED"; + + + protected KeycloakSession session; + protected RealmModel realm; + protected StorageProviderModel storageProviderModel; + + public AbstractUserAdapterFederatedStorage(KeycloakSession session, RealmModel realm, StorageProviderModel storageProviderModel) { + this.session = session; + this.realm = realm; + this.storageProviderModel = storageProviderModel; + } + + public UserFederatedStorageProvider getFederatedStorage() { + return session.userFederatedStorage(); + } + + @Override + public Set getRequiredActions() { + return getFederatedStorage().getRequiredActions(realm, this); + } + + @Override + public void addRequiredAction(String action) { + getFederatedStorage().addRequiredAction(realm, this, action); + + } + + @Override + public void removeRequiredAction(String action) { + getFederatedStorage().removeRequiredAction(realm, this, action); + + } + + @Override + public void addRequiredAction(RequiredAction action) { + getFederatedStorage().addRequiredAction(realm, this, action.name()); + + } + + @Override + public void removeRequiredAction(RequiredAction action) { + getFederatedStorage().removeRequiredAction(realm, this, action.name()); + } + + /** + * Get group membership mappings that are managed by this storage provider + * + * @return + */ + protected Set getGroupsInternal() { + return Collections.EMPTY_SET; + } + + /** + * Should the realm's default groups be appended to getGroups() call? + * If your storage provider is not managing group mappings then it is recommended that + * this method return true + * + * @return + */ + protected boolean appendDefaultGroups() { + return true; + } + + @Override + public Set getGroups() { + Set set = new HashSet<>(); + set.addAll(getFederatedStorage().getGroups(realm, this)); + if (appendDefaultGroups()) set.addAll(realm.getDefaultGroups()); + set.addAll(getGroupsInternal()); + return set; + } + + @Override + public void joinGroup(GroupModel group) { + getFederatedStorage().joinGroup(realm, this, group); + + } + + @Override + public void leaveGroup(GroupModel group) { + getFederatedStorage().leaveGroup(realm, this, group); + + } + + @Override + public boolean isMemberOf(GroupModel group) { + Set roles = getGroups(); + return KeycloakModelUtils.isMember(roles, group); + } + + @Override + public Set getRealmRoleMappings() { + Set roleMappings = getRoleMappings(); + + Set realmRoles = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof RealmModel) { + realmRoles.add(role); + } + } + return realmRoles; + } + + @Override + public Set getClientRoleMappings(ClientModel app) { + Set roleMappings = getRoleMappings(); + + Set roles = new HashSet(); + for (RoleModel role : roleMappings) { + RoleContainerModel container = role.getContainer(); + if (container instanceof ClientModel) { + ClientModel appModel = (ClientModel) container; + if (appModel.getId().equals(app.getId())) { + roles.add(role); + } + } + } + return roles; + } + + @Override + public boolean hasRole(RoleModel role) { + Set roles = getRoleMappings(); + return KeycloakModelUtils.hasRole(roles, role); + } + + @Override + public void grantRole(RoleModel role) { + getFederatedStorage().grantRole(realm, this, role); + + } + + /** + * Should the realm's default roles be appended to getRoleMappings() call? + * If your storage provider is not managing all role mappings then it is recommended that + * this method return true + * + * @return + */ + protected boolean appendDefaultRolesToRoleMappings() { + return true; + } + + protected Set getRoleMappingsInternal() { + return Collections.EMPTY_SET; + } + + @Override + public Set getRoleMappings() { + Set set = new HashSet<>(); + set.addAll(getFederatedRoleMappings()); + if (appendDefaultRolesToRoleMappings()) set.addAll(DefaultRoles.getDefaultRoles(realm)); + set.addAll(getRoleMappingsInternal()); + return set; + } + + protected Set getFederatedRoleMappings() { + return getFederatedStorage().getRoleMappings(realm, this); + } + + @Override + public void deleteRoleMapping(RoleModel role) { + getFederatedStorage().deleteRoleMapping(realm, this, role); + + } + + @Override + public boolean isEnabled() { + String val = getFirstAttribute(ENABLED_ATTRIBUTE); + if (val == null) return true; + else return Boolean.valueOf(val); + } + + @Override + public void setEnabled(boolean enabled) { + setSingleAttribute(ENABLED_ATTRIBUTE, Boolean.toString(enabled)); + } + + @Override + public boolean isOtpEnabled() { + String val = getFirstAttribute(OTP_ENABLED_ATTRIBUTE); + if (val == null) return false; + else return Boolean.valueOf(val); + } + + @Override + public void setOtpEnabled(boolean totp) { + setSingleAttribute(OTP_ENABLED_ATTRIBUTE, Boolean.toString(totp)); + + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public String getFederationLink() { + return null; + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public void setFederationLink(String link) { + + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public String getServiceAccountClientLink() { + return null; + } + + /** + * This method should not be overriden + * + * @return + */ + @Override + public void setServiceAccountClientLink(String clientInternalId) { + + } + + protected StorageId storageId; + + /** + * Defaults to 'f:' + storageProvider.getId() + ':' + getUsername() + * + * @return + */ + @Override + public String getId() { + if (storageId == null) { + storageId = new StorageId(storageProviderModel.getId(), getUsername()); + } + return storageId.getId(); + } + + @Override + public Long getCreatedTimestamp() { + String val = getFirstAttribute(CREATED_TIMESTAMP_ATTRIBUTE); + if (val == null) return null; + else return Long.valueOf(val); + } + + @Override + public void setCreatedTimestamp(Long timestamp) { + if (timestamp == null) { + setSingleAttribute(CREATED_TIMESTAMP_ATTRIBUTE, null); + } else { + setSingleAttribute(CREATED_TIMESTAMP_ATTRIBUTE, Long.toString(timestamp)); + } + + } + + @Override + public void setSingleAttribute(String name, String value) { + getFederatedStorage().setSingleAttribute(realm, this, name, value); + + } + + @Override + public void removeAttribute(String name) { + getFederatedStorage().removeAttribute(realm, this, name); + + } + + @Override + public void setAttribute(String name, List values) { + getFederatedStorage().setAttribute(realm, this, name, values); + + } + + @Override + public String getFirstAttribute(String name) { + return getFederatedStorage().getAttributes(realm, this).getFirst(name); + } + + @Override + public Map> getAttributes() { + return getFederatedStorage().getAttributes(realm, this); + } + + @Override + public List getAttribute(String name) { + return getFederatedStorage().getAttributes(realm, this).get(name); + } + + @Override + public String getFirstName() { + return getFirstAttribute(FIRST_NAME_ATTRIBUTE); + } + + @Override + public void setFirstName(String firstName) { + setSingleAttribute(FIRST_NAME_ATTRIBUTE, firstName); + + } + + @Override + public String getLastName() { + return getFirstAttribute(LAST_NAME_ATTRIBUTE); + } + + @Override + public void setLastName(String lastName) { + setSingleAttribute(LAST_NAME_ATTRIBUTE, lastName); + + } + + @Override + public String getEmail() { + return getFirstAttribute(EMAIL_ATTRIBUTE); + } + + @Override + public void setEmail(String email) { + setSingleAttribute(EMAIL_ATTRIBUTE, email); + + } + + @Override + public boolean isEmailVerified() { + String val = getFirstAttribute(EMAIL_VERIFIED_ATTRIBUTE); + if (val == null) return false; + else return Boolean.valueOf(val); + } + + @Override + public void setEmailVerified(boolean verified) { + setSingleAttribute(EMAIL_VERIFIED_ATTRIBUTE, Boolean.toString(verified)); + + } + + @Override + public void updateCredential(UserCredentialModel cred) { + getFederatedStorage().updateCredential(realm, this, cred); + + } + + @Override + public List getCredentialsDirectly() { + return getFederatedStorage().getCredentials(realm, this); + } + + @Override + public void updateCredentialDirectly(UserCredentialValueModel cred) { + getFederatedStorage().updateCredential(realm, this, cred); + + } +} diff --git a/server-spi/src/main/java/org/keycloak/storage/federated/CredentialModel.java b/server-spi/src/main/java/org/keycloak/storage/federated/CredentialModel.java deleted file mode 100644 index 7c4b735322..0000000000 --- a/server-spi/src/main/java/org/keycloak/storage/federated/CredentialModel.java +++ /dev/null @@ -1,24 +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.storage.federated; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class CredentialModel { -} diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialAuthenticationProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserCredentialAuthenticationProvider.java similarity index 82% rename from server-spi/src/main/java/org/keycloak/models/UserCredentialAuthenticationProvider.java rename to server-spi/src/main/java/org/keycloak/storage/user/UserCredentialAuthenticationProvider.java index a55145d08f..948ef4738b 100644 --- a/server-spi/src/main/java/org/keycloak/models/UserCredentialAuthenticationProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserCredentialAuthenticationProvider.java @@ -14,7 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.models; +package org.keycloak.storage.user; + +import org.keycloak.models.CredentialValidationOutput; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; import java.util.Set; diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialValidatorProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserCredentialValidatorProvider.java similarity index 82% rename from server-spi/src/main/java/org/keycloak/models/UserCredentialValidatorProvider.java rename to server-spi/src/main/java/org/keycloak/storage/user/UserCredentialValidatorProvider.java index 13fc0150cc..4c3d89790d 100644 --- a/server-spi/src/main/java/org/keycloak/models/UserCredentialValidatorProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserCredentialValidatorProvider.java @@ -14,7 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.models; +package org.keycloak.storage.user; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; import java.util.List; import java.util.Set; diff --git a/server-spi/src/main/java/org/keycloak/models/UserLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java similarity index 89% rename from server-spi/src/main/java/org/keycloak/models/UserLookupProvider.java rename to server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java index 3597f1e873..039986212f 100644 --- a/server-spi/src/main/java/org/keycloak/models/UserLookupProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java @@ -14,7 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.models; +package org.keycloak.storage.user; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; /** * @author Bill Burke diff --git a/server-spi/src/main/java/org/keycloak/models/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java similarity index 92% rename from server-spi/src/main/java/org/keycloak/models/UserQueryProvider.java rename to server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java index 57a69ca356..ceb4fe0cad 100644 --- a/server-spi/src/main/java/org/keycloak/models/UserQueryProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java @@ -14,7 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.models; +package org.keycloak.storage.user; + +import org.keycloak.models.GroupModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; import java.util.List; import java.util.Map; diff --git a/server-spi/src/main/java/org/keycloak/models/UserUpdateProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserUpdateProvider.java similarity index 88% rename from server-spi/src/main/java/org/keycloak/models/UserUpdateProvider.java rename to server-spi/src/main/java/org/keycloak/storage/user/UserUpdateProvider.java index a9efe970d2..72d18f2907 100644 --- a/server-spi/src/main/java/org/keycloak/models/UserUpdateProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserUpdateProvider.java @@ -14,7 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.keycloak.models; +package org.keycloak.storage.user; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; /** * @author Bill Burke diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java new file mode 100644 index 0000000000..2da879907f --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java @@ -0,0 +1,88 @@ +/* + * 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.testsuite.federation.storage; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.storage.StorageProviderModel; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserFederationStorageTest { + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + StorageProviderModel model = new StorageProviderModel(); + model.setDisplayName("user-props"); + model.setPriority(1); + model.setProviderName(UserPropertyFileStorageFactory.PROVIDER_ID); + appRealm.addStorageProvider(model); + } + }); + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected WebDriver driver; + + @WebResource + protected AppPage appPage; + + @WebResource + protected LoginPage loginPage; + + private void loginSuccessAndLogout(String username, String password) { + loginPage.open(); + loginPage.login(username, password); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + oauth.openLogout(); + } + + public void loginBadPassword(String username) { + loginPage.open(); + loginPage.login("username", "badpassword"); + Assert.assertEquals("Invalid username or password.", loginPage.getError()); + } + + + @Test + public void testLoginSuccess() { + loginSuccessAndLogout("tbrady", "goat"); + loginBadPassword("tbrady"); + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java new file mode 100644 index 0000000000..82a10c1cb6 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java @@ -0,0 +1,121 @@ +/* + * 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.testsuite.federation.storage; + +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.StorageId; +import org.keycloak.storage.StorageProvider; +import org.keycloak.storage.StorageProviderModel; +import org.keycloak.storage.adapter.AbstractUserAdapter; +import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; +import org.keycloak.storage.federated.UserFederatedStorageProvider; +import org.keycloak.storage.user.UserCredentialValidatorProvider; +import org.keycloak.storage.user.UserLookupProvider; + +import java.util.List; +import java.util.Properties; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserPropertyFileStorage implements UserLookupProvider, StorageProvider, UserCredentialValidatorProvider { + + protected Properties userPasswords; + protected StorageProviderModel model; + protected KeycloakSession session; + protected boolean federatedStorageEnabled; + + public UserPropertyFileStorage(KeycloakSession session, StorageProviderModel model, Properties userPasswords) { + this.session = session; + this.model = model; + this.userPasswords = userPasswords; + this.federatedStorageEnabled = model.getConfig().containsKey("USER_FEDERATED_STORAGE"); + } + + + @Override + public UserModel getUserById(String id, RealmModel realm) { + StorageId storageId = new StorageId(id); + final String username = storageId.getStorageId(); + if (!userPasswords.containsKey(username)) return null; + + return createUser(realm, username); + } + + private UserModel createUser(final RealmModel realm, final String username) { + return new AbstractUserAdapter(session, realm, model) { + @Override + public String getUsername() { + return username; + } + }; + } + + @Override + public UserModel getUserByUsername(String username, RealmModel realm) { + if (!userPasswords.containsKey(username)) return null; + + return createUser(realm, username); + } + + @Override + public UserModel getUserByEmail(String email, RealmModel realm) { + return null; + } + + @Override + public void preRemove(RealmModel realm) { + + } + + @Override + public void preRemove(RealmModel realm, GroupModel group) { + + } + + @Override + public void preRemove(RealmModel realm, RoleModel role) { + + } + + @Override + public void preRemove(RealmModel realm, StorageProviderModel model) { + + } + + @Override + public boolean validCredentials(KeycloakSession session, RealmModel realm, UserModel user, List input) { + for (UserCredentialModel cred : input) { + if (!cred.getType().equals(UserCredentialModel.PASSWORD)) return false; + String password = (String)userPasswords.get(user.getUsername()); + if (password == null) return false; + if (!password.equals(cred.getValue())) return false; + } + return true; + } + + @Override + public void close() { + + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java new file mode 100644 index 0000000000..0ebbce2462 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java @@ -0,0 +1,87 @@ +/* + * 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.testsuite.federation.storage; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.storage.StorageProvider; +import org.keycloak.storage.StorageProviderFactory; +import org.keycloak.storage.StorageProviderModel; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserPropertyFileStorageFactory implements StorageProviderFactory { + + + public static final String PROVIDER_ID = "user-password-props"; + + @Override + public boolean supports(Class type) { + return type.isAssignableFrom(UserPropertyFileStorage.class); + } + + @Override + public StorageProvider getInstance(KeycloakSession session, StorageProviderModel model) { + Properties props = new Properties(); + try { + props.load(getClass().getResourceAsStream("/storage-test/user-password.properties")); + } catch (IOException e) { + throw new RuntimeException(e); + } + return new UserPropertyFileStorage(session, model, props); + } + + @Override + public Set getConfigurationOptions() { + return null; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public StorageProvider create(KeycloakSession session) { + return null; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } +} diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.storage.StorageProviderFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.storage.StorageProviderFactory new file mode 100644 index 0000000000..001e16bdb1 --- /dev/null +++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.storage.StorageProviderFactory @@ -0,0 +1 @@ +org.keycloak.testsuite.federation.storage.UserPropertyFileStorageFactory \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index c2bc22c784..68671c3c50 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -21,7 +21,8 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n -log4j.logger.org.keycloak=info +log4j.logger.org.keycloak=debug + # Enable to view events # log4j.logger.org.keycloak.events=debug diff --git a/testsuite/integration/src/test/resources/storage-test/user-password.properties b/testsuite/integration/src/test/resources/storage-test/user-password.properties new file mode 100644 index 0000000000..1672680ecf --- /dev/null +++ b/testsuite/integration/src/test/resources/storage-test/user-password.properties @@ -0,0 +1 @@ +tbrady=goat \ No newline at end of file From 8b535c5da680874539d6048ccb2074d660918f0f Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 20 Jul 2016 11:16:20 -0400 Subject: [PATCH 2/2] log level --- testsuite/integration/src/test/resources/log4j.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index 68671c3c50..327102e806 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -21,7 +21,7 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n -log4j.logger.org.keycloak=debug +log4j.logger.org.keycloak=info # Enable to view events