keycloak-aplcache

Merge pull request #3599 from patriot1burke/master User

12/3/2016 12:20:45 AM

Changes

examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesStorageFactory.java 69(+0 -69)

examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesStorageProvider.java 72(+0 -72)

examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesStorageProvider.java 129(+0 -129)

examples/providers/user-storage-simple/src/main/resources/test2-users.properties 20(+0 -20)

Details

diff --git a/common/src/main/java/org/keycloak/common/util/EnvUtil.java b/common/src/main/java/org/keycloak/common/util/EnvUtil.java
index ff40c6f..b7b41c7 100755
--- a/common/src/main/java/org/keycloak/common/util/EnvUtil.java
+++ b/common/src/main/java/org/keycloak/common/util/EnvUtil.java
@@ -21,6 +21,8 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
+ * Replaces any ${} strings with their corresponding system property.
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
@@ -32,7 +34,7 @@ public final class EnvUtil {
     }
 
     /**
-     * Replaces any ${} strings with their corresponding environent variable.
+     * Replaces any ${} strings with their corresponding system property.
      *
      * @param val
      * @return
diff --git a/examples/providers/user-storage-jpa/README.md b/examples/providers/user-storage-jpa/README.md
index f965ef2..89eacad 100755
--- a/examples/providers/user-storage-jpa/README.md
+++ b/examples/providers/user-storage-jpa/README.md
@@ -10,4 +10,5 @@ Login and go to the User Federation tab and you should now see your deployed pro
 Add the provider, save it, then any new user you create will be stored and in the custom store you implemented.  You
 can modify the example and hot deploy it using the above maven command again.
 
-This example uses the built in in-memory datasource that comes with keycloak: ExampleDS.
+This example uses the built in in-memory datasource that comes with keycloak: ExampleDS.  NOTE!!! You'll have
+to change this to be an xa datasource for this to work.
diff --git a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml
index 51082e1..87b5028 100644
--- a/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml
+++ b/examples/providers/user-storage-jpa/src/main/resources/META-INF/persistence.xml
@@ -5,7 +5,7 @@
         http://java.sun.com/xml/ns/persistence
         http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
     <persistence-unit name="user-storage-jpa-example" transaction-type="JTA">
-        <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
+        <jta-data-source>java:jboss/datasources/KeycloakDS</jta-data-source>
 
         <class>org.keycloak.examples.storage.user.UserEntity</class>
 
diff --git a/examples/providers/user-storage-simple/README.md b/examples/providers/user-storage-simple/README.md
index 6549f8e..f194224 100755
--- a/examples/providers/user-storage-simple/README.md
+++ b/examples/providers/user-storage-simple/README.md
@@ -1,4 +1,4 @@
-Example User Federation Provider
+Example User Storage Provider
 ===================================================
 
 This is an example of user storage backed by a simple properties file.  This properties file only contains username/password
@@ -7,13 +7,10 @@ key pairs.  To deploy this provider you must have Keycloak running in standalone
         mvn clean install wildfly:deploy
 
 
+The "readonly-property-file" provider is hardcoded to look within the users.properties file embeded in the deployment jar
+THere is one user 'tbrady' with a password of 'superbowl'
 
-The ClasspathPropertiesStorageProvider is an example of a readonly provider.  If you go to the Users/Federation
-  page of the admin console you will see this provider listed under "classpath-properties.  To configure this provider you 
-specify a classpath to a properties file in the "path" field of the admin page for this plugin.  This example includes
-a "test-users.properties" within the JAR that you can use as the variable.
-  
-The FilePropertiesStorageProvider is an example of a writable provider.  It synchronizes changes made to
-username and password with the properties file.  If you go to the Users/Federation page of the admin console you will 
-see this provider listed under "file-properties".  To configure this provider you specify a fully qualified file path to 
-a properties file in the "path" field of the admin page for this plugin.  
+The "writeable-property-file" provider can be configured to point to a property file on disk.  It uses federated
+storage to augment the property file with any other information the user wants.
+
+Our developer guide walks through the implementation of both of these providers.
diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java
new file mode 100755
index 0000000..1b256c9
--- /dev/null
+++ b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.examples.userstorage.writeable;
+
+import org.keycloak.common.util.EnvUtil;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.credential.CredentialInput;
+import org.keycloak.credential.CredentialInputUpdater;
+import org.keycloak.credential.CredentialInputValidator;
+import org.keycloak.credential.CredentialModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.storage.StorageId;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
+import org.keycloak.storage.user.UserLookupProvider;
+import org.keycloak.storage.user.UserQueryProvider;
+import org.keycloak.storage.user.UserRegistrationProvider;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class PropertyFileUserStorageProvider implements
+        UserStorageProvider,
+        UserLookupProvider,
+        CredentialInputValidator,
+        CredentialInputUpdater,
+        UserRegistrationProvider,
+        UserQueryProvider {
+
+
+    public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD";
+
+    protected KeycloakSession session;
+    protected Properties properties;
+    protected ComponentModel model;
+    // map of loaded users in this transaction
+    protected Map<String, UserModel> loadedUsers = new HashMap<>();
+
+    public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
+        this.session = session;
+        this.model = model;
+        this.properties = properties;
+    }
+
+    // UserLookupProvider methods
+
+    @Override
+    public UserModel getUserByUsername(String username, RealmModel realm) {
+        UserModel adapter = loadedUsers.get(username);
+        if (adapter == null) {
+            String password = properties.getProperty(username);
+            if (password != null) {
+                adapter = createAdapter(realm, username);
+                loadedUsers.put(username, adapter);
+            }
+        }
+        return adapter;
+    }
+
+    protected UserModel createAdapter(RealmModel realm, String username) {
+        return new AbstractUserAdapterFederatedStorage(session, realm, model) {
+            @Override
+            public String getUsername() {
+                return username;
+            }
+
+            @Override
+            public void setUsername(String username) {
+                String pw = (String)properties.remove(username);
+                if (pw != null) {
+                    properties.put(username, pw);
+                    save();
+                }
+            }
+        };
+    }
+
+    @Override
+    public UserModel getUserById(String id, RealmModel realm) {
+        StorageId storageId = new StorageId(id);
+        String username = storageId.getExternalId();
+        return getUserByUsername(username, realm);
+    }
+
+    @Override
+    public UserModel getUserByEmail(String email, RealmModel realm) {
+        return null;
+    }
+
+    // UserQueryProvider methods
+
+    @Override
+    public int getUsersCount(RealmModel realm) {
+        return properties.size();
+    }
+
+    @Override
+    public List<UserModel> getUsers(RealmModel realm) {
+        return getUsers(realm, 0, Integer.MAX_VALUE);
+    }
+
+    @Override
+    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
+        List<UserModel> users = new LinkedList<>();
+        int i = 0;
+        for (Object obj : properties.keySet()) {
+            if (i++ < firstResult) continue;
+            String username = (String)obj;
+            UserModel user = getUserByUsername(username, realm);
+            users.add(user);
+            if (users.size() >= maxResults) break;
+        }
+        return users;
+    }
+
+    // UserQueryProvider method implementations
+
+    @Override
+    public List<UserModel> searchForUser(String search, RealmModel realm) {
+        return searchForUser(search, realm, 0, Integer.MAX_VALUE);
+    }
+
+    @Override
+    public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
+        List<UserModel> users = new LinkedList<>();
+        int i = 0;
+        for (Object obj : properties.keySet()) {
+            String username = (String)obj;
+            if (!username.contains(search)) continue;
+            if (i++ < firstResult) continue;
+            UserModel user = getUserByUsername(username, realm);
+            users.add(user);
+            if (users.size() >= maxResults) break;
+        }
+        return users;
+    }
+
+    @Override
+    public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
+        return searchForUser(params, realm, 0, Integer.MAX_VALUE);
+    }
+
+    @Override
+    public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
+        // only support searching by username
+        String usernameSearchString = params.get("username");
+        if (usernameSearchString == null) return Collections.EMPTY_LIST;
+        return searchForUser(usernameSearchString, realm, firstResult, maxResults);
+    }
+
+    @Override
+    public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
+        // runtime automatically handles querying UserFederatedStorage
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
+        // runtime automatically handles querying UserFederatedStorage
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
+        // runtime automatically handles querying UserFederatedStorage
+        return Collections.EMPTY_LIST;
+    }
+
+
+    // UserRegistrationProvider method implementations
+
+    public void save() {
+        String path = model.getConfig().getFirst("path");
+        path = EnvUtil.replace(path);
+        try {
+            FileOutputStream fos = new FileOutputStream(path);
+            properties.store(fos, "");
+            fos.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public UserModel addUser(RealmModel realm, String username) {
+        synchronized (properties) {
+            properties.setProperty(username, UNSET_PASSWORD);
+            save();
+        }
+        return createAdapter(realm, username);
+    }
+
+    @Override
+    public boolean removeUser(RealmModel realm, UserModel user) {
+        synchronized (properties) {
+            if (properties.remove(user.getUsername()) == null) return false;
+            save();
+            return true;
+        }
+    }
+
+
+
+
+
+    // CredentialInputValidator methods
+
+    @Override
+    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
+        String password = properties.getProperty(user.getUsername());
+        return credentialType.equals(CredentialModel.PASSWORD) && password != null;
+    }
+
+    @Override
+    public boolean supportsCredentialType(String credentialType) {
+        return credentialType.equals(CredentialModel.PASSWORD);
+    }
+
+    @Override
+    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
+        if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
+
+        UserCredentialModel cred = (UserCredentialModel)input;
+        String password = properties.getProperty(user.getUsername());
+        if (password == null || UNSET_PASSWORD.equals(password)) return false;
+        return password.equals(cred.getValue());
+    }
+
+    // CredentialInputUpdater methods
+
+    @Override
+    public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
+        if (!(input instanceof UserCredentialModel)) return false;
+        if (!input.getType().equals(CredentialModel.PASSWORD)) return false;
+        UserCredentialModel cred = (UserCredentialModel)input;
+        synchronized (properties) {
+            properties.setProperty(user.getUsername(), cred.getValue());
+            save();
+        }
+        return true;
+    }
+
+    @Override
+    public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
+        if (!credentialType.equals(CredentialModel.PASSWORD)) return;
+        synchronized (properties) {
+            properties.setProperty(user.getUsername(), UNSET_PASSWORD);
+            save();
+        }
+
+    }
+
+    private static final Set<String> disableableTypes = new HashSet<>();
+
+    static {
+        disableableTypes.add(CredentialModel.PASSWORD);
+    }
+
+    @Override
+    public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
+
+        return disableableTypes;
+    }
+    @Override
+    public void close() {
+
+    }
+}
diff --git a/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
index f203c13..fcb9380 100644
--- a/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
+++ b/examples/providers/user-storage-simple/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
@@ -1,2 +1,2 @@
-org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
-org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
\ No newline at end of file
+org.keycloak.examples.userstorage.readonly.PropertyFileUserStorageProviderFactory
+org.keycloak.examples.userstorage.writeable.PropertyFileUserStorageProviderFactory
\ No newline at end of file
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java
index 90f9b72..f98a439 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapper.java
@@ -25,7 +25,6 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleContainerModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
-import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.RoleUtils;
 import org.keycloak.models.utils.UserModelDelegate;
 import org.keycloak.storage.ldap.LDAPStorageProvider;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 830da5f..1c4393d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1810,6 +1810,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
         em.flush();
         setConfig(component, c);
+        ComponentUtil.notifyUpdated(session, this, component);
 
 
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 429d389..077296d 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1706,6 +1706,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
             }
         }
         updateRealm();
+        ComponentUtil.notifyUpdated(session, this, model);
 
     }
 
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
index d519286..da2f029 100644
--- a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
@@ -39,13 +39,22 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
         return null;
     }
 
-    void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException;
+    default
+    void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException
+    {
+
+    }
 
     default
     void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
 
     }
 
+    default
+    void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) {
+
+    }
+
     /**
      * These are config properties that are common across all implementation of this component type
      *
diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java b/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java
index 805fb25..f9838b4 100644
--- a/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java
+++ b/server-spi/src/main/java/org/keycloak/credential/CredentialInput.java
@@ -17,6 +17,8 @@
 package org.keycloak.credential;
 
 /**
+ *
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java b/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java
index 0e03bba..33c8230 100644
--- a/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java
+++ b/server-spi/src/main/java/org/keycloak/credential/CredentialInputValidator.java
@@ -22,6 +22,10 @@ import org.keycloak.models.UserModel;
 import java.util.List;
 
 /**
+ * Implentations of this interface can validate CredentialInput, i.e. verify a password.
+ * UserStorageProviders and CredentialProviders can implement this interface.
+ *
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java
index 260b0be..ab70808 100755
--- a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java
+++ b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java
@@ -22,6 +22,8 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserProvider;
 
 /**
+ * All these methods effect an entire cluster of Keycloak instances.
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
@@ -39,5 +41,10 @@ public interface UserCache extends UserProvider {
      * @param realm
      */
     void evict(RealmModel realm);
+
+    /**
+     * Clear cache entirely.
+     *
+     */
     void clear();
 }
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java
index c452e78..41f7430 100755
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java
@@ -21,24 +21,32 @@ import java.util.Arrays;
 import java.util.List;
 
 /**
-* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
-* @version $Revision: 1 $
-*/
+ * Configuration property metadata.  Used to render generic configuration pages for Keycloak extensions in the admin console.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
 public class ProviderConfigProperty {
     public static final String BOOLEAN_TYPE="boolean";
     public static final String STRING_TYPE="String";
 
-    // Possibility to configure multiple String values of any value (something like "redirect_uris" for clients)
+    /**
+     * Possibility to configure multiple String values of any value (something like "redirect_uris" for clients)
+     */
     public static final String MULTIVALUED_STRING_TYPE="MultivaluedString";
 
     public static final String SCRIPT_TYPE="Script";
     public static final String FILE_TYPE="File";
     public static final String ROLE_TYPE="Role";
 
-    // Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select)
+    /**
+     * Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select)
+     */
     public static final String LIST_TYPE="List";
 
-    // Possibility to configure multiple String values, which needs to be chosen from the list of predefined values (HTML select with multiple)
+    /**
+     * Possibility to configure multiple String values, which needs to be chosen from the list of predefined values (HTML select with multiple)
+     */
     public static final String MULTIVALUED_LIST_TYPE="MultivaluedList";
 
     public static final String CLIENT_LIST_TYPE="ClientList";
@@ -77,6 +85,11 @@ public class ProviderConfigProperty {
         this.secret = secret;
     }
 
+    /**
+     * Name of the config variable stored in the database
+     *
+     * @return
+     */
     public String getName() {
         return name;
     }
@@ -85,6 +98,11 @@ public class ProviderConfigProperty {
         this.name = name;
     }
 
+    /**
+     * Label shown in the admin console when configuring the variable
+     *
+     * @return
+     */
     public String getLabel() {
         return label;
     }
@@ -93,6 +111,12 @@ public class ProviderConfigProperty {
         this.label = label;
     }
 
+    /**
+     * Type of the variable.  i.e. boolean, string etc.  See the constants declared in this class for what your choices
+     * are.
+     *
+     * @return
+     */
     public String getType() {
         return type;
     }
@@ -101,6 +125,11 @@ public class ProviderConfigProperty {
         this.type = type;
     }
 
+    /**
+     * Default value for the variable
+     *
+     * @return
+     */
     public Object getDefaultValue() {
         return defaultValue;
     }
@@ -109,6 +138,11 @@ public class ProviderConfigProperty {
         this.defaultValue = defaultValue;
     }
 
+    /**
+     * For list types, this is a list of choices to choose from.
+     *
+     * @return
+     */
     public List<String> getOptions() {
         return options;
     }
@@ -117,6 +151,11 @@ public class ProviderConfigProperty {
         this.options = options;
     }
 
+    /**
+     * Help text that will be displayed in the admin console tooltip
+     *
+     * @return
+     */
     public String getHelpText() {
         return helpText;
     }
@@ -125,6 +164,12 @@ public class ProviderConfigProperty {
         this.helpText = helpText;
     }
 
+    /**
+     * If true, this variable is only writeable.  It will never be viewable.  This is important for things like
+     * passwords in which you never want to display them on the screen.
+     *
+     * @return
+     */
     public boolean isSecret() {
         return secret;
     }
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java
index 194e8e8..554eca1 100644
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java
@@ -22,6 +22,9 @@ import java.util.LinkedList;
 import java.util.List;
 
 /**
+ * Builds a list of ProviderConfigProperty instances.
+ *
+ *
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class ProviderConfigurationBuilder {
@@ -58,6 +61,11 @@ public class ProviderConfigurationBuilder {
         return this;
     }
 
+    /**
+     * Create the list.
+     *
+     * @return
+     */
     public List<ProviderConfigProperty> build() {
         return properties;
     }
@@ -77,42 +85,94 @@ public class ProviderConfigurationBuilder {
             return this;
         }
 
+        /**
+         * Label that will be shown for this configuration property in the admin console
+         *
+         * @param label
+         * @return
+         */
         public ProviderConfigPropertyBuilder label(String label) {
             this.label = label;
             return this;
         }
 
+        /**
+         * Help text that will be shown for this configuration property in the admin console
+         * when you hover over the tooltip
+         *
+         * @param helpText
+         * @return
+         */
         public ProviderConfigPropertyBuilder helpText(String helpText) {
             this.helpText = helpText;
             return this;
         }
 
+        /**
+         * Property type.  i.e. boolean, string.
+         * @see ProviderConfigProperty
+         *
+         *
+         * @param type
+         * @return
+         */
         public ProviderConfigPropertyBuilder type(String type) {
             this.type = type;
             return this;
         }
 
+        /**
+         * Default value that will be shown when configuring this property for the first time
+         *
+         * @param defaultValue
+         * @return
+         */
         public ProviderConfigPropertyBuilder defaultValue(Object defaultValue) {
             this.defaultValue = defaultValue;
             return this;
         }
 
+        /**
+         * If configuring a list type, these are the options you can choose from.
+         *
+         * @param options
+         * @return
+         */
         public ProviderConfigPropertyBuilder options(String... options) {
             this.options = Arrays.asList(options);
             return this;
         }
 
+        /**
+         * If configuring a list type, these are the options you can choose from.
+         *
+         * @param options
+         * @return
+         */
         public ProviderConfigPropertyBuilder options(List<String> options) {
             this.options = options;
             return this;
         }
 
 
+        /**
+         * If turned on, this property is only writable and never readable.
+         * This is useful for things like passwords where you never want an admin
+         * to be able to see what the password is.
+         *
+         * @param secret
+         * @return
+         */
         public ProviderConfigPropertyBuilder secret(boolean secret) {
             this.secret = secret;
             return this;
         }
 
+        /**
+         * Add the current property, and start building the next one
+         *
+         * @return
+         */
         public ProviderConfigurationBuilder add() {
             ProviderConfigProperty property = new ProviderConfigProperty();
             property.setName(name);
diff --git a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java
index c93dac4..fe9a41e 100644
--- a/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java
+++ b/server-spi/src/main/java/org/keycloak/storage/adapter/AbstractUserAdapterFederatedStorage.java
@@ -40,7 +40,7 @@ import java.util.Set;
  * of "f:" + providerId + ":" + getUsername().  UserModel properties like enabled, firstName, lastName, email, etc. are all
  * stored as attributes in federated storage.
  *
- * isEnabled() defaults to true if the ENABLED_ATTRIBUTE isn't set in federated
+ * isEnabled() defaults to true if the ENABLED_ATTRIBUTE isn't set in federated storage
  *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -116,6 +116,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         return true;
     }
 
+    /**
+     * Gets groups from federated storage and automatically appends default groups of realm.
+     * Also calls getGroupsInternal() method
+     * to pull group membership from provider.  Implementors can override that method
+     *
+     *
+     * @return
+     */
     @Override
     public Set<GroupModel> getGroups() {
         Set<GroupModel> set = new HashSet<>();
@@ -143,6 +151,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         return RoleUtils.isMember(roles, group);
     }
 
+    /**
+     * Gets role mappings from federated storage and automatically appends default roles.
+     * Also calls getRoleMappingsInternal() method
+     * to pull role mappings from provider.  Implementors can override that method
+     *
+     *
+     * @return
+     */
     @Override
     public Set<RoleModel> getRealmRoleMappings() {
         Set<RoleModel> roleMappings = getRoleMappings();
@@ -157,6 +173,14 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         return realmRoles;
     }
 
+    /**
+     * Gets role mappings from federated storage and automatically appends default roles.
+     * Also calls getRoleMappingsInternal() method
+     * to pull role mappings from provider.  Implementors can override that method
+     *
+     *
+     * @return
+     */
     @Override
     public Set<RoleModel> getClientRoleMappings(ClientModel app) {
         Set<RoleModel> roleMappings = getRoleMappings();
@@ -202,6 +226,13 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         return Collections.EMPTY_SET;
     }
 
+    /**
+     * Gets role mappings from federated storage and automatically appends default roles.
+     * Also calls getRoleMappingsInternal() method
+     * to pull role mappings from provider.  Implementors can override that method
+     *
+     * @return
+     */
     @Override
     public Set<RoleModel> getRoleMappings() {
         Set<RoleModel> set = new HashSet<>();
@@ -343,6 +374,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         return getFirstAttribute(FIRST_NAME_ATTRIBUTE);
     }
 
+    /**
+     * Stores as attribute in federated storage.
+     * FIRST_NAME_ATTRIBUTE
+     *
+     * @param firstName
+     */
     @Override
     public void setFirstName(String firstName) {
         setSingleAttribute(FIRST_NAME_ATTRIBUTE, firstName);
@@ -354,6 +391,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         return getFirstAttribute(LAST_NAME_ATTRIBUTE);
     }
 
+    /**
+     * Stores as attribute in federated storage.
+     * LAST_NAME_ATTRIBUTE
+     *
+     * @param lastName
+     */
     @Override
     public void setLastName(String lastName) {
         setSingleAttribute(LAST_NAME_ATTRIBUTE, lastName);
@@ -365,6 +408,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         return getFirstAttribute(EMAIL_ATTRIBUTE);
     }
 
+    /**
+     * Stores as attribute in federated storage.
+     * EMAIL_ATTRIBUTE
+     *
+     * @param email
+     */
     @Override
     public void setEmail(String email) {
         setSingleAttribute(EMAIL_ATTRIBUTE, email);
@@ -378,6 +427,12 @@ public abstract class AbstractUserAdapterFederatedStorage implements UserModel {
         else return Boolean.valueOf(val);
     }
 
+    /**
+     * Stores as attribute in federated storage.
+     * EMAIL_VERIFIED_ATTRIBUTE
+     *
+     * @param verified
+     */
     @Override
     public void setEmailVerified(boolean verified) {
         setSingleAttribute(EMAIL_VERIFIED_ATTRIBUTE, Boolean.toString(verified));
diff --git a/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java b/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java
index 0ceec66..7e374e3 100644
--- a/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java
+++ b/server-spi/src/main/java/org/keycloak/storage/user/ImportedUserValidation.java
@@ -28,7 +28,7 @@ import org.keycloak.models.UserModel;
  */
 public interface ImportedUserValidation {
     /**
-     * If this method returns null, then the user storage in local storage will be removed
+     * If this method returns null, then the user in local storage will be removed
      *
      * @param realm
      * @param user
diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java
index 0399862..ccbf46e 100644
--- a/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java
@@ -20,6 +20,9 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 
 /**
+ * Optional capability interface implemented by UserStorageProviders.  This interface is required
+ * if you want the UserStorageProvider to support basic login capabilities.
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
index 877b09f..eb179c9 100644
--- a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java
@@ -24,6 +24,10 @@ import java.util.List;
 import java.util.Map;
 
 /**
+ * Optional capability interface implemented by UserStorageProviders.
+ * Defines complex queries that are used to locate one or more users.  You must implement this interface
+ * if you want to view and manager users from the administration console.
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
@@ -99,10 +103,47 @@ public interface UserQueryProvider {
      */
     List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults);
 
+    /**
+     * Get users that belong to a specific group.  Implementations do not have to search in UserFederatedStorageProvider
+     * as this is done automatically.
+     *
+     * @see org.keycloak.storage.federated.UserFederatedStorageProvider
+     *
+     * @param realm
+     * @param group
+     * @param firstResult
+     * @param maxResults
+     * @return
+     */
     List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults);
+
+    /**
+     * Get users that belong to a specific group.  Implementations do not have to search in UserFederatedStorageProvider
+     * as this is done automatically.
+     *
+     * @see org.keycloak.storage.federated.UserFederatedStorageProvider
+     *
+     *
+     *
+     * @param realm
+     * @param group
+     * @return
+     */
     List<UserModel> getGroupMembers(RealmModel realm, GroupModel group);
 
+    /**
+     * Search for users that have a specific attribute with a specific value.
+     * Implementations do not have to search in UserFederatedStorageProvider
+     * as this is done automatically.
+     *
+     * @see org.keycloak.storage.federated.UserFederatedStorageProvider
+     *
 
-    // Searching by UserModel.attribute (not property)
+     *
+     * @param attrName
+     * @param attrValue
+     * @param realm
+     * @return
+     */
     List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm);
 }
diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java
index 25bb35e..b3ec00d 100644
--- a/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/user/UserRegistrationProvider.java
@@ -21,6 +21,9 @@ import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 
 /**
+ * Optional capability interface implemented by UserStorageProviders.
+ * Implement this interface if your provider supports adding and removing users.
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java
index 922c349..92d7c1a 100644
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProvider.java
@@ -28,9 +28,40 @@ import org.keycloak.provider.Provider;
 public interface UserStorageProvider extends Provider {
 
 
-    void preRemove(RealmModel realm);
-    void preRemove(RealmModel realm, GroupModel group);
-    void preRemove(RealmModel realm, RoleModel role);
+    /**
+     * Callback when a realm is removed.  Implement this if, for example, you want to do some
+     * cleanup in your user storage when a realm is removed
+     *
+     * @param realm
+     */
+    default
+    void preRemove(RealmModel realm) {
+
+    }
+
+    /**
+     * Callback when a group is removed.  Allows you to do things like remove a user
+     * group mapping in your external store if appropriate
+     *
+     * @param realm
+     * @param group
+     */
+    default
+    void preRemove(RealmModel realm, GroupModel group) {
+
+    }
+
+    /**
+     * Callback when a role is removed.  Allows you to do things like remove a user
+     * role mapping in your external store if appropriate
+
+     * @param realm
+     * @param role
+     */
+    default
+    void preRemove(RealmModel realm, RoleModel role) {
+
+    }
 
     /**
      * Optional type that can be used by implementations to
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
index 5efbb1c..969eacb 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ComponentUtil.java
@@ -89,5 +89,9 @@ public class ComponentUtil {
         ComponentFactory factory = getComponentFactory(session, model);
         factory.onCreate(session, realm, model);
     }
+    public static void notifyUpdated(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        ComponentFactory factory = getComponentFactory(session, model);
+        factory.onUpdate(session, realm, model);
+    }
 
 }
diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
index 0e48c55..906e225 100755
--- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -285,7 +285,9 @@ public class UserStorageManager implements UserProvider, OnUserCache {
     protected List<UserModel> importValidation(RealmModel realm, List<UserModel> users) {
         List<UserModel> tmp = new LinkedList<>();
         for (UserModel user : users) {
-            tmp.add(importValidation(realm, user));
+            UserModel model = importValidation(realm, user);
+            if (model == null) continue;
+            tmp.add(model);
         }
         return tmp;
     }