keycloak-memoizeit
Changes
server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java 419(+419 -0)
server-spi/src/main/java/org/keycloak/storage/user/UserCredentialAuthenticationProvider.java 7(+6 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserFederationStorageTest.java 88(+88 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorage.java 121(+121 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/UserPropertyFileStorageFactory.java 87(+87 -0)
Details
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 7802453..02403ee 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 d903799..982453e 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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/storage/adapter/AbstractUserAdapter.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapter.java
index 114b0c3..1d3ff73 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,147 +16,390 @@
*/
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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<String> 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<GroupModel> 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<GroupModel> getGroups() {
- return null;
+ Set<GroupModel> 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) {
+ Set<GroupModel> roles = getGroups();
+ return KeycloakModelUtils.isMember(roles, group);
+ }
+
+ @Override
+ public Set<RoleModel> getRealmRoleMappings() {
+ Set<RoleModel> roleMappings = getRoleMappings();
+
+ Set<RoleModel> realmRoles = new HashSet<RoleModel>();
+ for (RoleModel role : roleMappings) {
+ RoleContainerModel container = role.getContainer();
+ if (container instanceof RealmModel) {
+ realmRoles.add(role);
+ }
+ }
+ return realmRoles;
+ }
+
+ @Override
+ public Set<RoleModel> getClientRoleMappings(ClientModel app) {
+ Set<RoleModel> roleMappings = getRoleMappings();
+
+ Set<RoleModel> roles = new HashSet<RoleModel>();
+ 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<RoleModel> 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<RoleModel> getRoleMappingsInternal() {
+ return Collections.EMPTY_SET;
+ }
+
+ @Override
+ public Set<RoleModel> getRoleMappings() {
+ Set<RoleModel> 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 true;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ throw new ReadOnlyException("user is read only for this update");
+ }
+
+ @Override
+ public boolean isOtpEnabled() {
return false;
}
@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 Set<RoleModel> getRealmRoleMappings() {
+ 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<String> values) {
+ throw new ReadOnlyException("user is read only for this update");
+
+ }
+
+ @Override
+ public String getFirstAttribute(String name) {
return null;
}
@Override
- public Set<RoleModel> getClientRoleMappings(ClientModel app) {
+ public Map<String, List<String>> getAttributes() {
+ return new MultivaluedHashMap<>();
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
return null;
}
@Override
- public boolean hasRole(RoleModel role) {
- return false;
+ public String getFirstName() {
+ return null;
}
@Override
- public void grantRole(RoleModel role) {
+ public void setFirstName(String firstName) {
+ throw new ReadOnlyException("user is read only for this update");
}
@Override
- public Set<RoleModel> getRoleMappings() {
+ public String getLastName() {
return null;
}
@Override
- public void deleteRoleMapping(RoleModel role) {
+ public void setLastName(String lastName) {
+ throw new ReadOnlyException("user is read only for this update");
}
@Override
- public boolean isEnabled() {
- return false;
+ public String getEmail() {
+ return null;
}
@Override
- public void setEnabled(boolean enabled) {
+ public void setEmail(String email) {
+ throw new ReadOnlyException("user is read only for this update");
}
@Override
- public boolean isOtpEnabled() {
+ public boolean isEmailVerified() {
return false;
}
@Override
- public void setOtpEnabled(boolean totp) {
+ 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<UserCredentialValueModel> 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 0000000..bcc2651
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<String> 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<GroupModel> 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<GroupModel> getGroups() {
+ Set<GroupModel> 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<GroupModel> roles = getGroups();
+ return KeycloakModelUtils.isMember(roles, group);
+ }
+
+ @Override
+ public Set<RoleModel> getRealmRoleMappings() {
+ Set<RoleModel> roleMappings = getRoleMappings();
+
+ Set<RoleModel> realmRoles = new HashSet<RoleModel>();
+ for (RoleModel role : roleMappings) {
+ RoleContainerModel container = role.getContainer();
+ if (container instanceof RealmModel) {
+ realmRoles.add(role);
+ }
+ }
+ return realmRoles;
+ }
+
+ @Override
+ public Set<RoleModel> getClientRoleMappings(ClientModel app) {
+ Set<RoleModel> roleMappings = getRoleMappings();
+
+ Set<RoleModel> roles = new HashSet<RoleModel>();
+ 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<RoleModel> 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<RoleModel> getRoleMappingsInternal() {
+ return Collections.EMPTY_SET;
+ }
+
+ @Override
+ public Set<RoleModel> getRoleMappings() {
+ Set<RoleModel> set = new HashSet<>();
+ set.addAll(getFederatedRoleMappings());
+ if (appendDefaultRolesToRoleMappings()) set.addAll(DefaultRoles.getDefaultRoles(realm));
+ set.addAll(getRoleMappingsInternal());
+ return set;
+ }
+
+ protected Set<RoleModel> 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<String> values) {
+ getFederatedStorage().setAttribute(realm, this, name, values);
+
+ }
+
+ @Override
+ public String getFirstAttribute(String name) {
+ return getFederatedStorage().getAttributes(realm, this).getFirst(name);
+ }
+
+ @Override
+ public Map<String, List<String>> getAttributes() {
+ return getFederatedStorage().getAttributes(realm, this);
+ }
+
+ @Override
+ public List<String> 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<UserCredentialValueModel> 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/StorageId.java b/server-spi/src/main/java/org/keycloak/storage/StorageId.java
index 4ecac6e..5620740 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 6746d5d..56de3ab 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 f740f76..24f3c3d 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/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 0000000..2da8799
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 0000000..82a10c1
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<UserCredentialModel> 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 0000000..0ebbce2
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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<String> 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/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index c2bc22c..327102e 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -23,6 +23,7 @@ log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%
log4j.logger.org.keycloak=info
+
# Enable to view events
# log4j.logger.org.keycloak.events=debug
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 0000000..001e16b
--- /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/storage-test/user-password.properties b/testsuite/integration/src/test/resources/storage-test/user-password.properties
new file mode 100644
index 0000000..1672680
--- /dev/null
+++ b/testsuite/integration/src/test/resources/storage-test/user-password.properties
@@ -0,0 +1 @@
+tbrady=goat
\ No newline at end of file