keycloak-memoizeit

Merge pull request #3051 from patriot1burke/master user

7/20/2016 1:51:42 PM

Changes

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