keycloak-uncached

Changes

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java
index 76ba16d..1662fa2 100644
--- a/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ComponentTypeRepresentation.java
@@ -17,7 +17,9 @@
 
 package org.keycloak.representations.idm;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -27,6 +29,8 @@ public class ComponentTypeRepresentation {
     protected String helpText;
     protected List<ConfigPropertyRepresentation> properties;
 
+    protected Map<String, Object> metadata = new HashMap<>();
+
 
     public String getId() {
         return id;
@@ -51,4 +55,18 @@ public class ComponentTypeRepresentation {
     public void setProperties(List<ConfigPropertyRepresentation> properties) {
         this.properties = properties;
     }
+
+    /**
+     * Extra information about the component that might come from annotations or interfaces that the component implements
+     * For example, if UserStorageProvider implements ImportSynchronization
+     *
+     * @return
+     */
+    public Map<String, Object> getMetadata() {
+        return metadata;
+    }
+
+    public void setMetadata(Map<String, Object> metadata) {
+        this.metadata = metadata;
+    }
 }
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 2a9bd45..2c3348b 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
@@ -2063,6 +2063,9 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
     protected void setConfig(ComponentModel model, ComponentEntity c) {
         for (String key : model.getConfig().keySet()) {
             List<String> vals = model.getConfig().get(key);
+            if (vals == null) {
+                continue;
+            }
             for (String val : vals) {
                 ComponentConfigEntity config = new ComponentConfigEntity();
                 config.setId(KeycloakModelUtils.generateId());
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentModel.java b/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
index b46f5b6..6854c6d 100755
--- a/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
@@ -45,6 +45,7 @@ public class ComponentModel implements Serializable {
         this.name = copy.name;
         this.providerId = copy.providerId;
         this.providerType = copy.providerType;
+        this.parentId = copy.parentId;
         this.config = copy.config;
     }
 
diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java
index 0028ae7..d060c89 100755
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSessionFactory.java
@@ -34,6 +34,8 @@ public interface KeycloakSessionFactory extends ProviderEventManager {
 
     Set<Spi> getSpis();
 
+    Spi getSpi(Class<? extends Provider> providerClass);
+
     <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz);
 
     <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz, String id);
diff --git a/server-spi/src/main/java/org/keycloak/provider/Spi.java b/server-spi/src/main/java/org/keycloak/provider/Spi.java
index e4561b0..b9c47f8 100644
--- a/server-spi/src/main/java/org/keycloak/provider/Spi.java
+++ b/server-spi/src/main/java/org/keycloak/provider/Spi.java
@@ -17,6 +17,9 @@
 
 package org.keycloak.provider;
 
+import java.util.Collections;
+import java.util.List;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -26,5 +29,4 @@ public interface Spi {
     String getName();
     Class<? extends Provider> getProviderClass();
     Class<? extends ProviderFactory> getProviderFactoryClass();
-
 }
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
index ded1624..4cd038b 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderModel.java
@@ -44,8 +44,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
     public boolean isImportEnabled() {
         if (importEnabled == null) {
             String val = getConfig().getFirst("importEnabled");
-            if (val == null) importEnabled = false;
-            importEnabled = Boolean.valueOf(val);
+            if (val == null) {
+                importEnabled = true;
+            } else {
+                importEnabled = Boolean.valueOf(val);
+            }
         }
         return importEnabled;
 
@@ -59,8 +62,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
     public int getFullSyncPeriod() {
         if (fullSyncPeriod == null) {
             String val = getConfig().getFirst("fullSyncPeriod");
-            if (val == null) fullSyncPeriod = -1;
-            fullSyncPeriod = Integer.valueOf(val);
+            if (val == null) {
+                fullSyncPeriod = -1;
+            } else {
+                fullSyncPeriod = Integer.valueOf(val);
+            }
         }
         return fullSyncPeriod;
     }
@@ -73,8 +79,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
     public int getChangedSyncPeriod() {
         if (changedSyncPeriod == null) {
             String val = getConfig().getFirst("changedSyncPeriod");
-            if (val == null) changedSyncPeriod = -1;
-            changedSyncPeriod = Integer.valueOf(val);
+            if (val == null) {
+                changedSyncPeriod = -1;
+            } else {
+                changedSyncPeriod = Integer.valueOf(val);
+            }
         }
         return changedSyncPeriod;
     }
@@ -87,8 +96,11 @@ public class UserStorageProviderModel extends PrioritizedComponentModel {
     public int getLastSync() {
         if (lastSync == null) {
             String val = getConfig().getFirst("lastSync");
-            if (val == null) lastSync = 0;
-            lastSync = Integer.valueOf(val);
+            if (val == null) {
+                lastSync = 0;
+            } else {
+                lastSync = Integer.valueOf(val);
+            }
         }
         return lastSync;
     }
diff --git a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java
index 5801efd..9adbae6 100644
--- a/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java
+++ b/services/src/main/java/org/keycloak/credential/UserCredentialStoreManager.java
@@ -122,34 +122,36 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
             UserFederationProvider link = session.users().getFederationLink(realm, user);
             if (link != null) {
                 session.users().validateUser(realm, user);
-                Iterator<CredentialInput> it = toValidate.iterator();
-                while (it.hasNext()) {
-                    CredentialInput input = it.next();
-                    if (link.supportsCredentialType(input.getType())
-                            && link.isValid(realm, user, input)) {
-                        it.remove();
-                    }
+                validate(realm, user, toValidate, link);
+            }  // </deprecate>
+            else if (user.getFederationLink() != null) {
+                UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+                if (provider != null && provider instanceof CredentialInputValidator) {
+                    validate(realm, user, toValidate, ((CredentialInputValidator)provider));
                 }
             }
-            // </deprecate>
         }
 
         if (toValidate.isEmpty()) return true;
 
         List<CredentialInputValidator> credentialProviders = getCredentialProviders(realm, CredentialInputValidator.class);
         for (CredentialInputValidator validator : credentialProviders) {
-            Iterator<CredentialInput> it = toValidate.iterator();
-            while (it.hasNext()) {
-                CredentialInput input = it.next();
-                if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) {
-                    it.remove();
-                }
-            }
+            validate(realm, user, toValidate, validator);
 
         }
         return toValidate.isEmpty();
     }
 
+    private void validate(RealmModel realm, UserModel user, List<CredentialInput> toValidate, CredentialInputValidator validator) {
+        Iterator<CredentialInput> it = toValidate.iterator();
+        while (it.hasNext()) {
+            CredentialInput input = it.next();
+            if (validator.supportsCredentialType(input.getType()) && validator.isValid(realm, user, input)) {
+                it.remove();
+            }
+        }
+    }
+
     protected <T> List<T> getCredentialProviders(RealmModel realm, Class<T> type) {
         List<T> list = new LinkedList<T>();
         for (ProviderFactory f : session.getKeycloakSessionFactory().getProviderFactories(CredentialProvider.class)) {
@@ -178,6 +180,12 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
                 if (link.updateCredential(realm, user, input)) return;
             }
             // </deprecated>
+            else if (user.getFederationLink() != null) {
+                UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+                if (provider != null && provider instanceof CredentialInputUpdater) {
+                    if (((CredentialInputUpdater)provider).updateCredential(realm, user, input)) return;
+                }
+            }
         }
 
         List<CredentialInputUpdater> credentialProviders = getCredentialProviders(realm, CredentialInputUpdater.class);
@@ -203,6 +211,12 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
             if (link != null && link.getSupportedCredentialTypes().contains(credentialType)) {
                 link.disableCredentialType(realm, user, credentialType);
             }
+            else if (user.getFederationLink() != null) {
+                UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+                if (provider != null && provider instanceof CredentialInputUpdater) {
+                    ((CredentialInputUpdater)provider).disableCredentialType(realm, user, credentialType);
+                }
+            }
 
         }
 
@@ -233,6 +247,12 @@ public class UserCredentialStoreManager implements UserCredentialManager, OnUser
                 if (link.isConfiguredFor(realm, user, type)) return true;
             }
             // </deprecate>
+            else if (user.getFederationLink() != null) {
+                UserStorageProvider provider = UserStorageManager.getStorageProvider(session, realm, user.getFederationLink());
+                if (provider != null && provider instanceof CredentialInputValidator) {
+                    if (((CredentialInputValidator)provider).isConfiguredFor(realm, user, type)) return true;
+                }
+            }
 
         }
 
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index 9cdb065..caf281a 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -300,6 +300,14 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
     }
 
     @Override
+    public Spi getSpi(Class<? extends Provider> providerClass) {
+        for (Spi spi : spis) {
+            if (spi.getProviderClass().equals(providerClass)) return spi;
+        }
+        return null;
+    }
+
+    @Override
     public <T extends Provider> ProviderFactory<T> getProviderFactory(Class<T> clazz) {
          return getProviderFactory(clazz, provider.get(clazz));
     }
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 39abe85..058cdbc 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -49,6 +49,7 @@ import org.keycloak.representations.idm.OAuthClientRepresentation;
 import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.storage.UserStorageProviderModel;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -491,6 +492,13 @@ public class RealmManager implements RealmImporter {
             usersSyncManager.notifyToRefreshPeriodicSync(session, realm, fedProvider, false);
         }
 
+        // Refresh periodic sync tasks for configured storageProviders
+        List<UserStorageProviderModel> storageProviders = realm.getUserStorageProviders();
+        UserStorageSyncManager storageSync = new UserStorageSyncManager();
+        for (UserStorageProviderModel provider : storageProviders) {
+            storageSync.notifyToRefreshPeriodicSync(session, realm, provider, false);
+        }
+
         setupAuthorizationServices(realm);
         fireRealmPostCreate(realm);
 
diff --git a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
index de0363b..eaca98f 100755
--- a/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserStorageSyncManager.java
@@ -165,6 +165,11 @@ public class UserStorageSyncManager {
 
     // Ensure all cluster nodes are notified
     public void notifyToRefreshPeriodicSync(KeycloakSession session, RealmModel realm, UserStorageProviderModel provider, boolean removed) {
+        UserStorageProviderFactory factory = (UserStorageProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, provider.getProviderId());
+        if (!(factory instanceof ImportSynchronization) || !provider.isImportEnabled()) {
+            return;
+
+        }
         UserStorageProviderClusterEvent event = UserStorageProviderClusterEvent.createEvent(removed, realm.getId(), provider);
         session.getProvider(ClusterProvider.class).notify(USER_STORAGE_TASK_KEY, event);
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
index 9a87030..8120e7f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
@@ -50,6 +50,7 @@ import org.keycloak.representations.info.ServerInfoRepresentation;
 import org.keycloak.representations.info.SpiInfoRepresentation;
 import org.keycloak.representations.info.SystemInfoRepresentation;
 import org.keycloak.representations.info.ThemeInfoRepresentation;
+import org.keycloak.storage.user.ImportSynchronization;
 import org.keycloak.theme.Theme;
 import org.keycloak.theme.ThemeProvider;
 
@@ -135,6 +136,9 @@ public class ServerInfoAdminResource {
                         List<ProviderConfigProperty> configProperties = configured.getConfigProperties();
                         if (configProperties == null) configProperties = Collections.EMPTY_LIST;
                         rep.setProperties(ModelToRepresentation.toRepresentation(configProperties));
+                        if (pi instanceof ImportSynchronization) {
+                            rep.getMetadata().put("synchronizable", true);
+                        }
                         List<ComponentTypeRepresentation> reps = info.getComponentTypes().get(spi.getProviderClass().getName());
                         if (reps == null) {
                             reps = new LinkedList<>();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 47e0217..65cba8e 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -361,6 +361,14 @@ public class RealmAdminResource {
         return fed;
     }
 
+    @Path("user-storage")
+    public UserStorageProviderResource userStorage() {
+        UserStorageProviderResource fed = new UserStorageProviderResource(realm, auth, adminEvent);
+        ResteasyProviderFactory.getInstance().injectProperties(fed);
+        //resourceContext.initResource(fed);
+        return fed;
+    }
+
     @Path("authentication")
     public AuthenticationManagementResource flows() {
         AuthenticationManagementResource resource = new AuthenticationManagementResource(realm, session, auth, adminEvent);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java
new file mode 100644
index 0000000..1112416
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java
@@ -0,0 +1,124 @@
+/*
+ * 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.services.resources.admin;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.ServicesLogger;
+import org.keycloak.services.managers.UserStorageSyncManager;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.user.SynchronizationResult;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UserStorageProviderResource {
+    protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
+    protected RealmModel realm;
+
+    protected RealmAuth auth;
+
+    protected AdminEventBuilder adminEvent;
+
+    @Context
+    protected ClientConnection clientConnection;
+
+    @Context
+    protected UriInfo uriInfo;
+
+    @Context
+    protected KeycloakSession session;
+
+    @Context
+    protected HttpHeaders headers;
+
+    public UserStorageProviderResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
+        this.auth = auth;
+        this.realm = realm;
+        this.adminEvent = adminEvent;
+
+        auth.init(RealmAuth.Resource.USER);
+    }
+
+   /**
+     * Trigger sync of users
+     *
+     * @return
+     */
+    @POST
+    @Path("{id}/sync")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public SynchronizationResult syncUsers(@PathParam("id") String id,
+                                               @QueryParam("action") String action) {
+        auth.requireManage();
+
+        ComponentModel model = realm.getComponent(id);
+        if (model == null) {
+            throw new NotFoundException("Could not find component");
+        }
+        if (!model.getProviderType().equals(UserStorageProvider.class.getName())) {
+            throw new NotFoundException("found, but not a UserStorageProvider");
+        }
+
+        UserStorageProviderModel providerModel = new UserStorageProviderModel(model);
+
+
+
+        logger.debug("Syncing users");
+
+        UserStorageSyncManager syncManager = new UserStorageSyncManager();
+        SynchronizationResult syncResult;
+        if ("triggerFullSync".equals(action)) {
+            syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), providerModel);
+        } else if ("triggerChangedUsersSync".equals(action)) {
+            syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), providerModel);
+        } else {
+            throw new NotFoundException("Unknown action: " + action);
+        }
+
+        Map<String, Object> eventRep = new HashMap<>();
+        eventRep.put("action", action);
+        eventRep.put("result", syncResult);
+        adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(eventRep).success();
+
+        return syncResult;
+    }
+
+
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index f391b1a..642b5c1 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -44,6 +44,7 @@ import org.keycloak.services.ServicesLogger;
 import org.keycloak.services.filters.KeycloakTransactionCommitter;
 import org.keycloak.services.managers.ApplianceBootstrap;
 import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.UserStorageSyncManager;
 import org.keycloak.services.managers.UsersSyncManager;
 import org.keycloak.services.resources.admin.AdminRoot;
 import org.keycloak.services.scheduled.ClearExpiredEvents;
@@ -319,6 +320,7 @@ public class KeycloakApplication extends Application {
             timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredEvents(), interval), interval, "ClearExpiredEvents");
             timer.schedule(new ClusterAwareScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions(), interval), interval, "ClearExpiredUserSessions");
             new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer);
+            new UserStorageSyncManager().bootstrapPeriodic(sessionFactory, timer);
         } finally {
             session.close();
         }
diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
index 6061323..995043a 100755
--- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -38,6 +38,8 @@ import org.keycloak.models.cache.CachedUserModel;
 import org.keycloak.models.cache.OnUserCache;
 import org.keycloak.storage.federated.UserFederatedStorageProvider;
 import org.keycloak.credential.CredentialAuthentication;
+import org.keycloak.storage.user.ImportSynchronization;
+import org.keycloak.storage.user.ImportedUserValidation;
 import org.keycloak.storage.user.UserLookupProvider;
 import org.keycloak.storage.user.UserQueryProvider;
 import org.keycloak.storage.user.UserRegistrationProvider;
@@ -225,11 +227,38 @@ public class UserStorageManager implements UserProvider, OnUserCache {
         }
     }
 
+    /**
+     * Allows a UserStorageProvider to proxy and/or synchronize an imported user.
+     *
+     * @param realm
+     * @param user
+     * @return
+     */
+    protected UserModel importValidation(RealmModel realm, UserModel user) {
+        if (user == null || user.getFederationLink() == null) return user;
+        UserStorageProvider provider = getStorageProvider(session, realm, user.getFederationLink());
+        if (provider != null && provider instanceof ImportedUserValidation) {
+            return ((ImportedUserValidation)provider).validate(realm, user);
+        } else {
+            return user;
+        }
+
+    }
+
+    protected List<UserModel> importValidation(RealmModel realm, List<UserModel> users) {
+        List<UserModel> tmp = new LinkedList<>();
+        for (UserModel user : users) {
+            tmp.add(importValidation(realm, user));
+        }
+        return tmp;
+    }
+
     @Override
     public UserModel getUserById(String id, RealmModel realm) {
         StorageId storageId = new StorageId(id);
         if (storageId.getProviderId() == null) {
-            return localStorage().getUserById(id, realm);
+            UserModel user = localStorage().getUserById(id, realm);
+            return importValidation(realm, user);
         }
         UserLookupProvider provider = (UserLookupProvider)getStorageProvider(session, realm, storageId.getProviderId());
         return provider.getUserById(id, realm);
@@ -243,7 +272,9 @@ public class UserStorageManager implements UserProvider, OnUserCache {
     @Override
     public UserModel getUserByUsername(String username, RealmModel realm) {
         UserModel user = localStorage().getUserByUsername(username, realm);
-        if (user != null) return user;
+        if (user != null) {
+            return importValidation(realm, user);
+        }
         for (UserLookupProvider provider : getStorageProviders(session, realm, UserLookupProvider.class)) {
             user = provider.getUserByUsername(username, realm);
             if (user != null) return user;
@@ -257,7 +288,9 @@ public class UserStorageManager implements UserProvider, OnUserCache {
         if (user != null) return user;
         for (UserLookupProvider provider : getStorageProviders(session, realm, UserLookupProvider.class)) {
             user = provider.getUserByEmail(email, realm);
-            if (user != null) return user;
+            if (user != null) {
+                return importValidation(realm, user);
+            }
         }
         return null;
     }
@@ -266,7 +299,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
     public UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm) {
         UserModel user = localStorage().getUserByFederatedIdentity(socialLink, realm);
         if (user != null) {
-            return user;
+            return importValidation(realm, user);
         }
         if (getFederatedStorage() == null) return null;
         String id = getFederatedStorage().getUserByFederatedIdentity(socialLink, realm);
@@ -354,7 +387,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
 
     @Override
     public List<UserModel> getUsers(final RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) {
-        return query((provider, first, max) -> {
+        List<UserModel> results =  query((provider, first, max) -> {
             if (provider instanceof UserProvider) { // it is local storage
                 return ((UserProvider) provider).getUsers(realm, first, max, includeServiceAccounts);
             } else if (provider instanceof UserQueryProvider) {
@@ -364,6 +397,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
             return Collections.EMPTY_LIST;
         }
         , realm, firstResult, maxResults);
+        return importValidation(realm, results);
     }
 
     @Override
@@ -373,23 +407,26 @@ public class UserStorageManager implements UserProvider, OnUserCache {
 
     @Override
     public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
-        return query((provider, first, max) -> {
+        List<UserModel> results = query((provider, first, max) -> {
             if (provider instanceof UserQueryProvider) {
                 return ((UserQueryProvider)provider).searchForUser(search, realm, first, max);
 
             }
             return Collections.EMPTY_LIST;
         }, realm, firstResult, maxResults);
+        return importValidation(realm, results);
+
     }
 
     @Override
     public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm) {
-        return searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
+        List<UserModel> results = searchForUser(attributes, realm, 0, Integer.MAX_VALUE - 1);
+        return importValidation(realm, results);
     }
 
     @Override
     public List<UserModel> searchForUser(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
-        return query((provider, first, max) -> {
+        List<UserModel> results = query((provider, first, max) -> {
             if (provider instanceof UserQueryProvider) {
                 return ((UserQueryProvider)provider).searchForUser(attributes, realm, first, max);
 
@@ -397,6 +434,8 @@ public class UserStorageManager implements UserProvider, OnUserCache {
             return Collections.EMPTY_LIST;
         }
         , realm, firstResult, maxResults);
+        return importValidation(realm, results);
+
     }
 
     @Override
@@ -417,7 +456,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
             }
             return Collections.EMPTY_LIST;
         }, realm,0, Integer.MAX_VALUE - 1);
-        return results;
+        return importValidation(realm, results);
     }
 
     @Override
@@ -472,7 +511,7 @@ public class UserStorageManager implements UserProvider, OnUserCache {
             }
             return Collections.EMPTY_LIST;
         }, realm, firstResult, maxResults);
-        return results;
+        return importValidation(realm, results);
     }
 
 
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
index 291f084..02f97cd 100644
--- 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
@@ -22,8 +22,12 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.storage.UserStorageProviderFactory;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.user.ImportSynchronization;
+import org.keycloak.storage.user.SynchronizationResult;
 
 import java.io.IOException;
+import java.util.Date;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Properties;
@@ -32,7 +36,7 @@ import java.util.Properties;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UserPropertyFileStorageFactory implements UserStorageProviderFactory<UserPropertyFileStorage> {
+public class UserPropertyFileStorageFactory implements UserStorageProviderFactory<UserPropertyFileStorage>, ImportSynchronization {
 
 
     public static final String PROVIDER_ID = "user-password-props";
@@ -80,4 +84,14 @@ public class UserPropertyFileStorageFactory implements UserStorageProviderFactor
     public void close() {
 
     }
+
+    @Override
+    public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+        return SynchronizationResult.ignored();
+    }
+
+    @Override
+    public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+        return SynchronizationResult.ignored();
+    }
 }
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index b262f65..e5cf535 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -700,7 +700,8 @@ module.controller('UserFederationCtrl', function($scope, $location, $route, real
     };
 });
 
-module.controller('GenericUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm, serverInfo, instance, providerId, Components) {
+module.controller('GenericUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
+                                                     serverInfo, instance, providerId, Components, UserStorageSync) {
     console.log('GenericUserStorageCtrl');
     console.log('providerId: ' + providerId);
     $scope.create = !instance.providerId;
@@ -719,6 +720,7 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
 
     }
     $scope.provider = instance;
+    $scope.showSync = false;
 
     console.log("providerFactory: " + providerFactory.id);
 
@@ -733,6 +735,12 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
             };
             instance.config['priority'] = ["0"];
 
+            $scope.fullSyncEnabled = false;
+            $scope.changedSyncEnabled = false;
+            if (providerFactory.metadata.synchronizable) {
+                instance.config['fullSyncPeriod'] = ['-1'];
+                instance.config['changedSyncPeriod'] = ['-1'];
+            }
             if (providerFactory.properties) {
 
                 for (var i = 0; i < providerFactory.properties.length; i++) {
@@ -747,6 +755,20 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
             }
 
         } else {
+            $scope.fullSyncEnabled = (instance.config['fullSyncPeriod'] && instance.config['fullSyncPeriod'][0] > 0);
+            $scope.changedSyncEnabled = (instance.config['changedSyncPeriod'] && instance.config['changedSyncPeriod'][0]> 0);
+            if (providerFactory.metadata.synchronizable) {
+                if (!instance.config['fullSyncPeriod']) {
+                    console.log('setting to -1');
+                    instance.config['fullSyncPeriod'] = ['-1'];
+
+                }
+                if (!instance.config['changedSyncPeriod']) {
+                    console.log('setting to -1');
+                    instance.config['changedSyncPeriod'] = ['-1'];
+
+                }
+            }
             /*
             console.log('Manage instance');
             console.log(instance.name);
@@ -758,6 +780,13 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
             }
             */
         }
+        if (providerFactory.metadata.synchronizable) {
+            if (instance.config && instance.config['importEnabled']) {
+                $scope.showSync = instance.config['importEnabled'][0] == 'true';
+            } else {
+                $scope.showSync = true;
+            }
+        }
 
         $scope.changed = false;
     }
@@ -773,6 +802,25 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
 
     }, true);
 
+    $scope.$watch('fullSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
+
+        $scope.instance.config['fullSyncPeriod'][0] = $scope.fullSyncEnabled ? "604800" : "-1";
+        $scope.changed = true;
+    });
+
+    $scope.$watch('changedSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
+
+        $scope.instance.config['changedSyncPeriod'][0] = $scope.changedSyncEnabled ? "86400" : "-1";
+        $scope.changed = true;
+    });
+
+
     $scope.save = function() {
         $scope.changed = false;
         if ($scope.create) {
@@ -814,6 +862,27 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
             $route.reload();
         }
     };
+
+    $scope.triggerFullSync = function() {
+        console.log('GenericCtrl: triggerFullSync');
+        triggerSync('triggerFullSync');
+    }
+
+    $scope.triggerChangedUsersSync = function() {
+        console.log('GenericCtrl: triggerChangedUsersSync');
+        triggerSync('triggerChangedUsersSync');
+    }
+
+    function triggerSync(action) {
+        UserStorageSync.save({ action: action, realm: $scope.realm.realm, componentId: $scope.instance.id }, {}, function(syncResult) {
+            $route.reload();
+            Notifications.success("Sync of users finished successfully. " + syncResult.status);
+        }, function() {
+            $route.reload();
+            Notifications.error("Error during sync of users");
+        });
+    }
+
 });
 
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index 47c2b04..4e1c436 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1667,4 +1667,11 @@ module.factory('Components', function($resource) {
     });
 });
 
+module.factory('UserStorageSync', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/user-storage/:componentId/sync', {
+        realm : '@realm',
+        componentId : '@componentId'
+    });
+});
+
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
index e3ff5f6..68b6489 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-generic.html
@@ -35,6 +35,39 @@
 
         </fieldset>
 
+        <fieldset ng-show="showSync">
+            <legend><span class="text">{{:: 'sync-settings' | translate}}</span></legend>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="fullSyncEnabled">{{:: 'periodic-full-sync' | translate}}</label>
+                <div class="col-md-6">
+                    <input ng-model="fullSyncEnabled" name="fullSyncEnabled" id="fullSyncEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'periodic-full-sync.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="fullSyncEnabled">
+                <label class="col-md-2 control-label" for="fullSyncPeriod">{{:: 'full-sync-period' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text"  ng-model="instance.config['fullSyncPeriod'][0]" id="fullSyncPeriod" />
+                </div>
+                <kc-tooltip>{{:: 'full-sync-period.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="changedSyncEnabled">{{:: 'periodic-changed-users-sync' | translate}}</label>
+                <div class="col-md-6">
+                    <input ng-model="changedSyncEnabled" name="changedSyncEnabled" id="changedSyncEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'periodic-changed-users-sync.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="changedSyncEnabled">
+                <label class="col-md-2 control-label" for="changedSyncPeriod">{{:: 'changed-users-sync-period' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text"  ng-model="instance.config['changedSyncPeriod'][0]" id="changedSyncPeriod" />
+                </div>
+                <kc-tooltip>{{:: 'changed-users-sync-period.tooltip' | translate}}</kc-tooltip>
+            </div>
+        </fieldset>
+
+
         <div class="form-group">
             <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
                 <button kc-save>{{:: 'save' | translate}}</button>
@@ -46,6 +79,8 @@
             <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageUsers">
                 <button kc-save  data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
                 <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+                <button class="btn btn-primary" data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed || !showSync">{{:: 'synchronize-changed-users' | translate}}</button>
+                <button class="btn btn-primary" data-ng-click="triggerFullSync()" data-ng-hide="changed || !showSync">{{:: 'synchronize-all-users' | translate}}</button>
             </div>
         </div>
     </form>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
index cd0df94..e5f6273 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
@@ -41,7 +41,7 @@
         </div>
 
         <div class="col-md-6" data-ng-if="option.type == 'Script'">
-            <div ng-model="config[option.name]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
+            <div ng-model="config[option.name][0]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
                 {{config[option.name]}}
             </div>
         </div>