keycloak-aplcache

Sync - first iteration

8/8/2014 4:01:34 PM

Changes

export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportUtils.java 46(+0 -46)

Details

diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java
index 04845fb..b0d9925 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java
@@ -2,12 +2,19 @@ package org.keycloak.examples.federation.properties;
 
 import org.keycloak.Config;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderFactory;
 import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Properties;
 import java.util.Set;
@@ -84,4 +91,44 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
     public void close() {
 
     }
+
+    @Override
+    public void syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
+        KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+            @Override
+            public void run(KeycloakSession session) {
+                RealmModel realm = session.realms().getRealm(realmId);
+                BasePropertiesFederationProvider federationProvider = (BasePropertiesFederationProvider)getInstance(session, model);
+                Set<String> allUsernames = federationProvider.getProperties().stringPropertyNames();
+                for (String username : allUsernames) {
+                    federationProvider.getUserByUsername(realm, username);
+                }
+            }
+
+        });
+    }
+
+    @Override
+    public void syncChangedUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model, Date lastSync) {
+        KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+            @Override
+            public void run(KeycloakSession session) {
+                RealmModel realm = session.realms().getRealm(realmId);
+                UserProvider localProvider = session.userStorage();
+                BasePropertiesFederationProvider federationProvider = (BasePropertiesFederationProvider)getInstance(session, model);
+                Set<String> allUsernames = federationProvider.getProperties().stringPropertyNames();
+                for (String username : allUsernames) {
+                    UserModel localUser = localProvider.getUserByUsername(username, realm);
+
+                    if (localUser == null) {
+                        // New user, let's import him
+                        federationProvider.getUserByUsername(realm, username);
+                    }
+                }
+            }
+
+        });
+    }
 }
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java
index 5c64b73..2f89575 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java
@@ -14,6 +14,8 @@ import java.util.Properties;
  */
 public class FilePropertiesFederationFactory extends BasePropertiesFederationFactory {
 
+    public static final String PROVIDER_NAME = "file-properties";
+
     @Override
     protected BasePropertiesFederationProvider createProvider(KeycloakSession session, UserFederationProviderModel model, Properties props) {
         return new FilePropertiesFederationProvider(session, props, model);
@@ -35,6 +37,6 @@ public class FilePropertiesFederationFactory extends BasePropertiesFederationFac
      */
     @Override
     public String getId() {
-        return "file-properties";
+        return PROVIDER_NAME;
     }
 }
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportSessionTask.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportSessionTask.java
new file mode 100644
index 0000000..185a966
--- /dev/null
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportSessionTask.java
@@ -0,0 +1,25 @@
+package org.keycloak.exportimport.util;
+
+import java.io.IOException;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionTask;
+
+/**
+ * Just to wrap {@link IOException}
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class ExportImportSessionTask implements KeycloakSessionTask {
+
+    @Override
+    public void run(KeycloakSession session) {
+        try {
+            runExportImportTask(session);
+        } catch (IOException ioe) {
+            throw new RuntimeException("Error during export/import: " + ioe.getMessage(), ioe);
+        }
+    }
+
+    protected abstract void runExportImportTask(KeycloakSession session) throws IOException;
+}
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ImportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ImportUtils.java
index aa4d83d..edc5b52 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ImportUtils.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ImportUtils.java
@@ -77,7 +77,7 @@ public class ImportUtils {
             // We just imported master realm. All 'masterAdminApps' need to be refreshed
             RealmModel adminRealm = realm;
             for (RealmModel currentRealm : model.getRealms()) {
-                ApplicationModel masterApp = adminRealm.getApplicationByName(ExportImportUtils.getMasterRealmAdminApplicationName(currentRealm));
+                ApplicationModel masterApp = adminRealm.getApplicationByName(KeycloakModelUtils.getMasterRealmAdminApplicationName(currentRealm));
                 if (masterApp != null) {
                     currentRealm.setMasterAdminApp(masterApp);
                 }  else {
@@ -87,7 +87,7 @@ public class ImportUtils {
         } else {
             // Need to refresh masterApp for current realm
             RealmModel adminRealm = model.getRealm(adminRealmId);
-            ApplicationModel masterApp = adminRealm.getApplicationByName(ExportImportUtils.getMasterRealmAdminApplicationName(realm));
+            ApplicationModel masterApp = adminRealm.getApplicationByName(KeycloakModelUtils.getMasterRealmAdminApplicationName(realm));
             if (masterApp != null) {
                 realm.setMasterAdminApp(masterApp);
             }  else {
@@ -113,7 +113,7 @@ public class ImportUtils {
             adminRole = adminRealm.getRole(AdminRoles.ADMIN);
         }
 
-        ApplicationModel realmAdminApp = KeycloakModelUtils.createApplication(adminRealm, ExportImportUtils.getMasterRealmAdminApplicationName(realm));
+        ApplicationModel realmAdminApp = KeycloakModelUtils.createApplication(adminRealm, KeycloakModelUtils.getMasterRealmAdminApplicationName(realm));
         realmAdminApp.setBearerOnly(true);
         realm.setMasterAdminApp(realmAdminApp);
 
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java
index 5899757..2f66d09 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/MultipleStepsExportProvider.java
@@ -6,8 +6,10 @@ import org.keycloak.exportimport.ExportProvider;
 import org.keycloak.exportimport.UsersExportStrategy;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.idm.RealmRepresentation;
 
 import java.io.IOException;
@@ -24,7 +26,7 @@ public abstract class MultipleStepsExportProvider implements ExportProvider {
     public void exportModel(KeycloakSessionFactory factory) throws IOException {
         final RealmsHolder holder = new RealmsHolder();
 
-        ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+        KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() {
 
             @Override
             public void run(KeycloakSession session) {
@@ -46,10 +48,10 @@ public abstract class MultipleStepsExportProvider implements ExportProvider {
         final UsersHolder usersHolder = new UsersHolder();
         final boolean exportUsersIntoRealmFile = usersExportStrategy == UsersExportStrategy.REALM_FILE;
 
-        ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+        KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
             @Override
-            public void run(KeycloakSession session) throws IOException {
+            protected void runExportImportTask(KeycloakSession session) throws IOException {
                 RealmModel realm = session.realms().getRealmByName(realmName);
                 RealmRepresentation rep = ExportUtils.exportRealm(session, realm, exportUsersIntoRealmFile);
                 writeRealm(realmName + "-realm.json", rep);
@@ -77,10 +79,10 @@ public abstract class MultipleStepsExportProvider implements ExportProvider {
                     usersHolder.currentPageEnd = usersHolder.totalCount;
                 }
 
-                ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+                KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
                     @Override
-                    public void run(KeycloakSession session) throws IOException {
+                    protected void runExportImportTask(KeycloakSession session) throws IOException {
                         RealmModel realm = session.realms().getRealmByName(realmName);
                         usersHolder.users = session.users().getUsers(realm, usersHolder.currentPageStart, usersHolder.currentPageEnd - usersHolder.currentPageStart);
 
diff --git a/export-import/export-import-dir/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java b/export-import/export-import-dir/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java
index 4c532f0..7399574 100755
--- a/export-import/export-import-dir/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java
+++ b/export-import/export-import-dir/src/main/java/org/keycloak/exportimport/dir/DirImportProvider.java
@@ -4,11 +4,12 @@ import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.exportimport.ImportProvider;
 import org.keycloak.exportimport.Strategy;
-import org.keycloak.exportimport.util.ExportImportJob;
-import org.keycloak.exportimport.util.ExportImportUtils;
+import org.keycloak.exportimport.util.ExportImportSessionTask;
 import org.keycloak.exportimport.util.ImportUtils;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.util.JsonSerialization;
 
@@ -91,10 +92,10 @@ public class DirImportProvider implements ImportProvider {
         FileInputStream is = new FileInputStream(realmFile);
         final RealmRepresentation realmRep = JsonSerialization.readValue(is, RealmRepresentation.class);
 
-        ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+        KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
             @Override
-            public void run(KeycloakSession session) throws IOException {
+            public void runExportImportTask(KeycloakSession session) throws IOException {
                 ImportUtils.importRealm(session, realmRep, strategy);
             }
 
@@ -103,10 +104,10 @@ public class DirImportProvider implements ImportProvider {
         // Import users
         for (File userFile : userFiles) {
             final FileInputStream fis = new FileInputStream(userFile);
-            ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+            KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
                 @Override
-                public void run(KeycloakSession session) throws IOException {
+                protected void runExportImportTask(KeycloakSession session) throws IOException {
                     ImportUtils.importUsersFromStream(session, realmName, JsonSerialization.mapper, fis);
                 }
             });
diff --git a/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java b/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java
index 17cec51..a8d7d98 100755
--- a/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java
+++ b/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileExportProvider.java
@@ -3,12 +3,13 @@ package org.keycloak.exportimport.singlefile;
 import org.codehaus.jackson.map.ObjectMapper;
 import org.jboss.logging.Logger;
 import org.keycloak.exportimport.ExportProvider;
-import org.keycloak.exportimport.util.ExportImportJob;
-import org.keycloak.exportimport.util.ExportImportUtils;
+import org.keycloak.exportimport.util.ExportImportSessionTask;
 import org.keycloak.exportimport.util.ExportUtils;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.util.JsonSerialization;
 
@@ -38,10 +39,10 @@ public class SingleFileExportProvider implements ExportProvider {
     @Override
     public void exportModel(KeycloakSessionFactory factory) throws IOException {
         logger.infof("Exporting model into file %s", this.file.getAbsolutePath());
-        ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+        KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
             @Override
-            public void run(KeycloakSession session) throws IOException {
+            protected void runExportImportTask(KeycloakSession session) throws IOException {
                 List<RealmModel> realms = session.realms().getRealms();
                 List<RealmRepresentation> reps = new ArrayList<RealmRepresentation>();
                 for (RealmModel realm : realms) {
@@ -58,10 +59,10 @@ public class SingleFileExportProvider implements ExportProvider {
     @Override
     public void exportRealm(KeycloakSessionFactory factory, final String realmName) throws IOException {
         logger.infof("Exporting realm '%s' into file %s", realmName, this.file.getAbsolutePath());
-        ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+        KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
             @Override
-            public void run(KeycloakSession session) throws IOException {
+            protected void runExportImportTask(KeycloakSession session) throws IOException {
                 RealmModel realm = session.realms().getRealmByName(realmName);
                 RealmRepresentation realmRep = ExportUtils.exportRealm(session, realm, true);
                 writeToFile(realmRep);
diff --git a/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileImportProvider.java b/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileImportProvider.java
index 6fbe1c8..a1e1109 100755
--- a/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileImportProvider.java
+++ b/export-import/export-import-single-file/src/main/java/org/keycloak/exportimport/singlefile/SingleFileImportProvider.java
@@ -3,16 +3,15 @@ package org.keycloak.exportimport.singlefile;
 import org.jboss.logging.Logger;
 import org.keycloak.exportimport.ImportProvider;
 import org.keycloak.exportimport.Strategy;
-import org.keycloak.exportimport.util.ExportImportJob;
-import org.keycloak.exportimport.util.ExportImportUtils;
 import org.keycloak.exportimport.util.ImportUtils;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.util.JsonSerialization;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import org.keycloak.exportimport.util.ExportImportSessionTask;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.util.JsonSerialization;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -30,10 +29,10 @@ public class SingleFileImportProvider implements ImportProvider {
     @Override
     public void importModel(KeycloakSessionFactory factory, final Strategy strategy) throws IOException {
         logger.infof("Full importing from file %s", this.file.getAbsolutePath());
-        ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+        KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
             @Override
-            public void run(KeycloakSession session) throws IOException {
+            protected void runExportImportTask(KeycloakSession session) throws IOException {
                 FileInputStream is = new FileInputStream(file);
                 ImportUtils.importFromStream(session, JsonSerialization.mapper, is, strategy);
             }
diff --git a/export-import/export-import-zip/src/main/java/org/keycloak/exportimport/zip/ZipImportProvider.java b/export-import/export-import-zip/src/main/java/org/keycloak/exportimport/zip/ZipImportProvider.java
index d9a93ee..69cfd46 100755
--- a/export-import/export-import-zip/src/main/java/org/keycloak/exportimport/zip/ZipImportProvider.java
+++ b/export-import/export-import-zip/src/main/java/org/keycloak/exportimport/zip/ZipImportProvider.java
@@ -8,11 +8,12 @@ import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.exportimport.ImportProvider;
 import org.keycloak.exportimport.Strategy;
-import org.keycloak.exportimport.util.ExportImportJob;
-import org.keycloak.exportimport.util.ExportImportUtils;
+import org.keycloak.exportimport.util.ExportImportSessionTask;
 import org.keycloak.exportimport.util.ImportUtils;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.util.JsonSerialization;
 
@@ -81,10 +82,10 @@ public class ZipImportProvider implements ImportProvider {
             ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
             final RealmRepresentation realmRep = JsonSerialization.mapper.readValue(bis, RealmRepresentation.class);
 
-            ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+            KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
                 @Override
-                public void run(KeycloakSession session) throws IOException {
+                protected void runExportImportTask(KeycloakSession session) throws IOException {
                     ImportUtils.importRealm(session, realmRep, strategy);
                 }
 
@@ -99,10 +100,10 @@ public class ZipImportProvider implements ImportProvider {
                     this.decrypter.extractEntry(entry, bos, this.password);
                     final ByteArrayInputStream bis2 = new ByteArrayInputStream(bos.toByteArray());
 
-                    ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
+                    KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
 
                         @Override
-                        public void run(KeycloakSession session) throws IOException {
+                        protected void runExportImportTask(KeycloakSession session) throws IOException {
                             ImportUtils.importUsersFromStream(session, realmName, JsonSerialization.mapper, bis2);
                         }
                     });
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
index fed574b..85a049f 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
@@ -292,4 +292,29 @@ public class LDAPFederationProvider implements UserFederationProvider {
     public void close() {
         //To change body of implemented methods use File | Settings | File Templates.
     }
+
+    protected void importPicketlinkUsers(RealmModel realm, List<User> users, UserFederationProviderModel fedModel) {
+        for (User picketlinkUser : users) {
+            String username = picketlinkUser.getLoginName();
+            UserModel currentUser = session.userStorage().getUserByUsername(username, realm);
+
+            if (currentUser == null) {
+                // Add new user to Keycloak
+                importUserFromPicketlink(realm, picketlinkUser);
+                logger.infof("Added new user from LDAP: " + username);
+            } else {
+                if ((fedModel.getId().equals(currentUser.getFederationLink())) && (picketlinkUser.getId().equals(currentUser.getAttribute(LDAPFederationProvider.LDAP_ID)))) {
+                    // Update keycloak user
+                    String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null;
+                    currentUser.setEmail(email);
+                    currentUser.setFirstName(picketlinkUser.getFirstName());
+                    currentUser.setLastName(picketlinkUser.getLastName());
+                    logger.infof("Updated user from LDAP: " + currentUser.getUsername());
+                } else {
+                    // TODO: We have local user of same username like LDAP user, but not linked. What to do? Delete him and import again?
+                    throw new IllegalStateException("User " + username + " has invalid LDAP ID or doesn't have federation link");
+                }
+            }
+        }
+    }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
index 53aeb0c..88bf66f 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
@@ -1,14 +1,26 @@
 package org.keycloak.federation.ldap;
 
+import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderFactory;
 import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.picketlink.PartitionManagerProvider;
+import org.picketlink.idm.IdentityManager;
 import org.picketlink.idm.PartitionManager;
+import org.picketlink.idm.model.IdentityType;
+import org.picketlink.idm.model.basic.User;
+import org.picketlink.idm.query.IdentityQuery;
 
 import java.util.Collections;
+import java.util.Date;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -17,6 +29,7 @@ import java.util.Set;
  * @version $Revision: 1 $
  */
 public class LDAPFederationProviderFactory implements UserFederationProviderFactory {
+    private static final Logger logger = Logger.getLogger(LDAPFederationProviderFactory.class);
     public static final String PROVIDER_NAME = "ldap";
 
     @Override
@@ -25,7 +38,7 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
     }
 
     @Override
-    public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
+    public LDAPFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
         PartitionManagerProvider idmProvider = session.getProvider(PartitionManagerProvider.class);
         PartitionManager partition = idmProvider.getPartitionManager(model);
         return new LDAPFederationProvider(session, model, partition);
@@ -49,4 +62,76 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
     public Set<String> getConfigurationOptions() {
         return Collections.emptySet();
     }
+
+    @Override
+    public void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) {
+        logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s, current time: " + new Date(), realmId, model.getDisplayName());
+
+        PartitionManagerProvider idmProvider = sessionFactory.create().getProvider(PartitionManagerProvider.class);
+        PartitionManager partitionMgr = idmProvider.getPartitionManager(model);
+        IdentityQuery<User> userQuery = partitionMgr.createIdentityManager().createIdentityQuery(User.class);
+        syncImpl(sessionFactory, userQuery, realmId, model);
+
+        // TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
+    }
+
+    @Override
+    public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
+        logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, current time: " + new Date() + ", last sync time: " + lastSync, realmId, model.getDisplayName());
+
+        PartitionManagerProvider idmProvider = sessionFactory.create().getProvider(PartitionManagerProvider.class);
+        PartitionManager partitionMgr = idmProvider.getPartitionManager(model);
+
+        // Sync newly created users
+        IdentityManager identityManager = partitionMgr.createIdentityManager();
+        IdentityQuery<User> userQuery = identityManager.createIdentityQuery(User.class)
+                .setParameter(IdentityType.CREATED_AFTER, lastSync);
+        syncImpl(sessionFactory, userQuery, realmId, model);
+
+        // Sync updated users
+        userQuery = identityManager.createIdentityQuery(User.class)
+                .setParameter(IdentityType.MODIFIED_AFTER, lastSync);
+        syncImpl(sessionFactory, userQuery, realmId, model);
+    }
+
+    protected void syncImpl(KeycloakSessionFactory sessionFactory, IdentityQuery<User> userQuery, final String realmId, final UserFederationProviderModel fedModel) {
+        boolean pagination = Boolean.parseBoolean(fedModel.getConfig().get(LDAPConstants.PAGINATION));
+
+        if (pagination) {
+            String pageSizeConfig = fedModel.getConfig().get(LDAPConstants.BATCH_SIZE_FOR_SYNC);
+            int pageSize = pageSizeConfig!=null ? Integer.parseInt(pageSizeConfig) : LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC;
+            boolean nextPage = true;
+            while (nextPage) {
+                userQuery.setLimit(pageSize);
+                final List<User> users = userQuery.getResultList();
+                nextPage = userQuery.getPaginationContext() != null;
+
+                KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+                    @Override
+                    public void run(KeycloakSession session) {
+                        importPicketlinkUsers(session, realmId, fedModel, users);
+                    }
+
+                });
+            }
+        } else {
+            // LDAP pagination not available. Do everything in single transaction
+            final List<User> users = userQuery.getResultList();
+            KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+                @Override
+                public void run(KeycloakSession session) {
+                    importPicketlinkUsers(session, realmId, fedModel, users);
+                }
+
+            });
+        }
+    }
+
+    protected void importPicketlinkUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List<User> users) {
+        RealmModel realm = session.realms().getRealm(realmId);
+        LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel);
+        ldapFedProvider.importPicketlinkUsers(realm, users, fedModel);
+    }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
index d67c499..f862d03 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
@@ -29,6 +29,17 @@ public class LDAPUtils {
         return picketlinkUser;
     }
 
+    public static User updateUser(PartitionManager partitionManager, String username, String firstName, String lastName, String email) {
+        IdentityManager idmManager = getIdentityManager(partitionManager);
+        User picketlinkUser = BasicModel.getUser(idmManager, username);
+        picketlinkUser.setFirstName(firstName);
+        picketlinkUser.setLastName(lastName);
+        picketlinkUser.setEmail(email);
+        picketlinkUser.setAttribute(new Attribute("fullName", getFullName(username, firstName, lastName)));
+        idmManager.update(picketlinkUser);
+        return picketlinkUser;
+    }
+
     public static void updatePassword(PartitionManager partitionManager, User picketlinkUser, String password) {
         IdentityManager idmManager = getIdentityManager(partitionManager);
         idmManager.updateCredential(picketlinkUser, new Password(password.toCharArray()));
@@ -48,10 +59,6 @@ public class LDAPUtils {
         }
     }
 
-    public static boolean isUserExists(PartitionManager partitionManager, String username) {
-        return getUser(partitionManager, username) != null;
-    }
-
     public static User getUser(PartitionManager partitionManager, String username) {
         IdentityManager idmManager = getIdentityManager(partitionManager);
         return BasicModel.getUser(idmManager, username);
diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
index 1328c01..38b538e 100644
--- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
+++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java
@@ -22,5 +22,9 @@ public class LDAPConstants {
     public static final String CONNECTION_POOLING = "connectionPooling";
     public static final String PAGINATION = "pagination";
 
+    // Count of users processed per single transaction during sync process
+    public static final String BATCH_SIZE_FOR_SYNC = "batchSizeForSync";
+    public static final int DEFAULT_BATCH_SIZE_FOR_SYNC = 1000;
+
     public static final String USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE = "userAccountControlsAfterPasswordUpdate";
 }
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java b/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java
index 956779f..4247649 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java
@@ -2,6 +2,7 @@ package org.keycloak.models;
 
 import org.keycloak.provider.ProviderFactory;
 
+import java.util.Date;
 import java.util.Set;
 
 /**
@@ -32,4 +33,26 @@ public interface UserFederationProviderFactory extends ProviderFactory<UserFeder
      */
     @Override
     String getId();
+
+    /**
+     * Sync all users from the provider storage to Keycloak storage.
+     *
+     * @param sessionFactory
+     * @param realmId
+     * @param model
+     */
+    void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model);
+
+    /**
+     * Sync just changed (added / updated / removed) users from the provider storage to Keycloak storage. This is useful in case
+     * that your storage supports "changelogs" (Tracking what users changed since specified date). It's implementation specific to
+     * decide what exactly will be changed (For example LDAP supports tracking of added / updated users, but not removed users. So
+     * removed users are not synced)
+     *
+     * @param sessionFactory
+     * @param realmId
+     * @param model
+     * @param lastSync
+     */
+    void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync);
 }
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index ca07fdb..2f329b7 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -5,6 +5,9 @@ import org.keycloak.models.ApplicationModel;
 import org.keycloak.models.ClaimMask;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.KeycloakTransaction;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
@@ -132,4 +135,36 @@ public final class KeycloakModelUtils {
         }
         return user;
     }
+
+    /**
+     * Wrap given runnable job into KeycloakTransaction.
+     *
+     * @param factory
+     * @param task
+     */
+    public static void runJobInTransaction(KeycloakSessionFactory factory, KeycloakSessionTask task) {
+        KeycloakSession session = factory.create();
+        KeycloakTransaction tx = session.getTransaction();
+        try {
+            tx.begin();
+            task.run(session);
+
+            if (tx.isActive()) {
+                if (tx.getRollbackOnly()) {
+                    tx.rollback();
+                } else {
+                    tx.commit();
+                }
+            }
+        } finally {
+            if (tx.isActive()) {
+                tx.rollback();
+            }
+            session.close();
+        }
+    }
+
+    public static String getMasterRealmAdminApplicationName(RealmModel realm) {
+        return realm.getName() + "-realm";
+    }
 }
diff --git a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java
index ec1ba58..6fc5237 100755
--- a/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java
+++ b/picketlink/keycloak-picketlink-ldap/src/main/java/org/keycloak/picketlink/ldap/PartitionManagerRegistry.java
@@ -11,13 +11,17 @@ import org.picketlink.idm.config.LDAPMappingConfigurationBuilder;
 import org.picketlink.idm.config.LDAPStoreConfigurationBuilder;
 import org.picketlink.idm.internal.DefaultPartitionManager;
 import org.picketlink.idm.model.basic.User;
-
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.ConcurrentHashMap;
 
-import static org.picketlink.common.constants.LDAPConstants.*;
+import static org.picketlink.common.constants.LDAPConstants.CN;
+import static org.picketlink.common.constants.LDAPConstants.EMAIL;
+import static org.picketlink.common.constants.LDAPConstants.SN;
+import static org.picketlink.common.constants.LDAPConstants.UID;
+import static org.picketlink.common.constants.LDAPConstants.CREATE_TIMESTAMP;
+import static org.picketlink.common.constants.LDAPConstants.MODIFY_TIMESTAMP;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -80,6 +84,8 @@ public class PartitionManagerRegistry {
         }
 
         String ldapFirstNameMapping = activeDirectory ?  "givenName" : CN;
+        String createTimestampMapping = activeDirectory ? "whenCreated" : CREATE_TIMESTAMP;
+        String modifyTimestampMapping = activeDirectory ? "whenChanged" : MODIFY_TIMESTAMP;
         String[] userObjectClasses = getUserObjectClasses(ldapConfig);
 
         boolean pagination = ldapConfig.containsKey(LDAPConstants.PAGINATION) ? Boolean.parseBoolean(ldapConfig.get(LDAPConstants.PAGINATION)) : false;
@@ -112,7 +118,9 @@ public class PartitionManagerRegistry {
                 .attribute("loginName", ldapLoginNameMapping, true)
                 .attribute("firstName", ldapFirstNameMapping)
                 .attribute("lastName", SN)
-                .attribute("email", EMAIL);
+                .attribute("email", EMAIL)
+                .readOnlyAttribute("createdDate", createTimestampMapping)
+                .readOnlyAttribute("modifyDate", modifyTimestampMapping);
 
         if (activeDirectory && ldapLoginNameMapping.equals("sAMAccountName")) {
             ldapUserMappingBuilder.bindingAttribute("fullName", CN);
diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java
index 04ac0a1..753409a 100755
--- a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java
+++ b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java
@@ -38,7 +38,7 @@ public class DummyUserFederationProvider implements UserFederationProvider {
 
     @Override
     public boolean removeUser(RealmModel realm, UserModel user) {
-        return true;
+        return users.remove(user.getUsername()) != null;
     }
 
     @Override
diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProviderFactory.java b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProviderFactory.java
index a5a0b6e..d20ef87 100755
--- a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProviderFactory.java
+++ b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProviderFactory.java
@@ -2,10 +2,11 @@ package org.keycloak.testutils;
 
 import org.keycloak.Config;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderFactory;
 import org.keycloak.models.UserFederationProviderModel;
-
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -45,4 +46,12 @@ public class DummyUserFederationProviderFactory implements UserFederationProvide
     public String getId() {
         return "dummy";
     }
+
+    @Override
+    public void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) {
+    }
+
+    @Override
+    public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
+    }
 }
diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/LDAPEmbeddedServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/LDAPEmbeddedServer.java
index 466952a..1c10b24 100755
--- a/testsuite/integration/src/main/java/org/keycloak/testutils/LDAPEmbeddedServer.java
+++ b/testsuite/integration/src/main/java/org/keycloak/testutils/LDAPEmbeddedServer.java
@@ -42,6 +42,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
     protected String vendor = LDAPConstants.VENDOR_OTHER;
     protected boolean connectionPooling = true;
     protected boolean pagination = true;
+    protected int batchSizeForSync = LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC;
 
     public static String IDM_TEST_LDAP_CONNECTION_URL = "idm.test.ldap.connection.url";
     public static String IDM_TEST_LDAP_BASE_DN = "idm.test.ldap.base.dn";
@@ -55,6 +56,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
     public static String IDM_TEST_LDAP_VENDOR = "idm.test.ldap.vendor";
     public static String IDM_TEST_LDAP_CONNECTION_POOLING = "idm.test.ldap.connection.pooling";
     public static String IDM_TEST_LDAP_PAGINATION = "idm.test.ldap.pagination";
+    public static String IDM_TEST_LDAP_BATCH_SIZE_FOR_SYNC = "idm.test.ldap.batch.size.for.sync";
 
 
     public LDAPEmbeddedServer() {
@@ -84,6 +86,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
         vendor = p.getProperty(IDM_TEST_LDAP_VENDOR);
         connectionPooling = Boolean.parseBoolean(p.getProperty(IDM_TEST_LDAP_CONNECTION_POOLING, "true"));
         pagination = Boolean.parseBoolean(p.getProperty(IDM_TEST_LDAP_PAGINATION, "true"));
+        batchSizeForSync = Integer.parseInt(p.getProperty(IDM_TEST_LDAP_BATCH_SIZE_FOR_SYNC, String.valueOf(batchSizeForSync)));
     }
 
     @Override
@@ -134,6 +137,7 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
         ldapConfig.put(LDAPConstants.VENDOR, getVendor());
         ldapConfig.put(LDAPConstants.CONNECTION_POOLING, String.valueOf(isConnectionPooling()));
         ldapConfig.put(LDAPConstants.PAGINATION, String.valueOf(isPagination()));
+        ldapConfig.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, String.valueOf(getBatchSizeForSync()));
         return ldapConfig;
     }
 
@@ -216,6 +220,10 @@ public class LDAPEmbeddedServer extends AbstractLDAPTest {
         return pagination;
     }
 
+    public int getBatchSizeForSync() {
+        return batchSizeForSync;
+    }
+
     @Override
     public void importLDIF(String fileName) throws Exception {
         // import LDIF only in case we are running against embedded LDAP server
diff --git a/testsuite/integration/src/main/resources/ldap/ldap-connection.properties b/testsuite/integration/src/main/resources/ldap/ldap-connection.properties
index 6119852..c275c2f 100644
--- a/testsuite/integration/src/main/resources/ldap/ldap-connection.properties
+++ b/testsuite/integration/src/main/resources/ldap/ldap-connection.properties
@@ -8,4 +8,5 @@ idm.test.ldap.start.embedded.ldap.server=true
 idm.test.ldap.bind.dn=uid\=admin,ou\=system
 idm.test.ldap.bind.credential=secret
 idm.test.ldap.connection.pooling=true
-idm.test.ldap.pagination=true
\ No newline at end of file
+idm.test.ldap.pagination=true
+idm.test.ldap.batch.size.for.sync=3
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
index 791e686..1e8e9ce 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
@@ -102,7 +102,7 @@ public class FederationProvidersIntegrationTest {
     @WebResource
     protected AccountPasswordPage changePasswordPage;
 
-    private static UserModel addUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
+    static UserModel addUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
         UserModel user = session.users().addUser(realm, username);
         user.setEmail(email);
         user.setEnabled(true);
@@ -313,7 +313,7 @@ public class FederationProvidersIntegrationTest {
         }
     }
 
-    private static PartitionManager getPartitionManager(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
+    static PartitionManager getPartitionManager(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
         PartitionManagerProvider partitionManagerProvider = keycloakSession.getProvider(PartitionManagerProvider.class);
         return partitionManagerProvider.getPartitionManager(ldapFedModel);
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java
new file mode 100644
index 0000000..8105769
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java
@@ -0,0 +1,145 @@
+package org.keycloak.testsuite.forms;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.federation.ldap.LDAPUtils;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationProvider;
+import org.keycloak.models.UserFederationProviderFactory;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserProvider;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+import org.keycloak.testutils.LDAPEmbeddedServer;
+import org.picketlink.idm.PartitionManager;
+import org.picketlink.idm.model.basic.User;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SyncProvidersTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static UserFederationProviderModel ldapModel = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            LDAPEmbeddedServer ldapServer = ldapRule.getEmbeddedServer();
+            Map<String,String> ldapConfig = ldapServer.getLDAPConfig();
+            ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "false");
+            ldapConfig.put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.UNSYNCED.toString());
+
+            ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap");
+
+            // Delete all LDAP users and add 5 new users for testing
+            PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(manager.getSession(), ldapModel);
+            LDAPUtils.removeAllUsers(partitionManager);
+
+            User user1 = LDAPUtils.addUser(partitionManager, "user1", "User1FN", "User1LN", "user1@email.org");
+            LDAPUtils.updatePassword(partitionManager, user1, "password1");
+            User user2 = LDAPUtils.addUser(partitionManager, "user2", "User2FN", "User2LN", "user2@email.org");
+            LDAPUtils.updatePassword(partitionManager, user2, "password2");
+            User user3 = LDAPUtils.addUser(partitionManager, "user3", "User3FN", "User3LN", "user3@email.org");
+            LDAPUtils.updatePassword(partitionManager, user3, "password3");
+            User user4 = LDAPUtils.addUser(partitionManager, "user4", "User4FN", "User4LN", "user4@email.org");
+            LDAPUtils.updatePassword(partitionManager, user4, "password4");
+            User user5 = LDAPUtils.addUser(partitionManager, "user5", "User5FN", "User5LN", "user5@email.org");
+            LDAPUtils.updatePassword(partitionManager, user5, "password5");
+
+            // Add properties provider
+//            Map<String,String> filePropertiesConfig = new HashMap<String, String>();
+//            filePropertiesConfig.put("path", );
+//            appRealm.addUserFederationProvider(FilePropertiesFederationFactory.PROVIDER_NAME, filePropertiesConfig, 1, "test-fileProps");
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    @Test
+    public void testLDAPSync() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            UserFederationProviderFactory ldapFedFactory = (UserFederationProviderFactory)sessionFactory.getProviderFactory(UserFederationProvider.class, LDAPFederationProviderFactory.PROVIDER_NAME);
+            ldapFedFactory.syncAllUsers(sessionFactory, "test", ldapModel);
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+
+        // Assert users imported (model test)
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+            UserProvider userProvider = session.userStorage();
+            assertUserImported(userProvider, testRealm, "user1", "User1FN", "User1LN", "user1@email.org");
+            assertUserImported(userProvider, testRealm, "user2", "User2FN", "User2LN", "user2@email.org");
+            assertUserImported(userProvider, testRealm, "user3", "User3FN", "User3LN", "user3@email.org");
+            assertUserImported(userProvider, testRealm, "user4", "User4FN", "User4LN", "user4@email.org");
+            assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org");
+
+            // wait a bit
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException ie) {
+                throw new RuntimeException(ie);
+            }
+            Date beforeLDAPUpdate = new Date();
+
+            // Add user to LDAP and update 'user5' in LDAP
+            PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(session, ldapModel);
+            LDAPUtils.addUser(partitionManager, "user6", "User6FN", "User6LN", "user6@email.org");
+            LDAPUtils.updateUser(partitionManager, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
+
+            // Assert still old users in local provider
+            assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org");
+            Assert.assertNull(userProvider.getUserByUsername("user6", testRealm));
+
+            // Trigger partial sync
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            UserFederationProviderFactory ldapFedFactory = (UserFederationProviderFactory)sessionFactory.getProviderFactory(UserFederationProvider.class, LDAPFederationProviderFactory.PROVIDER_NAME);
+            ldapFedFactory.syncChangedUsers(sessionFactory, "test", ldapModel, beforeLDAPUpdate);
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+            UserProvider userProvider = session.userStorage();
+            // Assert users updated in local provider
+            assertUserImported(userProvider, testRealm, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
+            assertUserImported(userProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org");
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    private void assertUserImported(UserProvider userProvider, RealmModel realm, String username, String expectedFirstName, String expectedLastName, String expectedEmail) {
+        UserModel user = userProvider.getUserByUsername(username, realm);
+        Assert.assertNotNull(user);
+        Assert.assertEquals(expectedFirstName, user.getFirstName());
+        Assert.assertEquals(expectedLastName, user.getLastName());
+        Assert.assertEquals(expectedEmail, user.getEmail());
+    }
+}