keycloak-uncached

Changes

Details

diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoKeycloakTransaction.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoKeycloakTransaction.java
index 705b888..a8dffc7 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoKeycloakTransaction.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/MongoKeycloakTransaction.java
@@ -42,11 +42,13 @@ public class MongoKeycloakTransaction implements KeycloakTransaction {
         } catch (MongoException e) {
             throw MongoStoreImpl.convertException(e);
         }
+        started = false;
     }
 
     @Override
     public void rollback() {
         invocationContext.rollback();
+        started = false;
     }
 
     @Override
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 b0d9925..d79a13b 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
@@ -101,24 +101,7 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
                 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);
 
@@ -131,4 +114,9 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
 
         });
     }
+
+    @Override
+    public void syncChangedUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model, Date lastSync) {
+        syncAllUsers(sessionFactory, realmId, model);
+    }
 }
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js
index 706cd0e..52a933f 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js
@@ -356,25 +356,50 @@ module.controller('UserFederationCtrl', function($scope, $location, realm, UserF
 
 });
 
-module.controller('GenericUserFederationCtrl', function($scope, $location, Notifications, Dialog, realm, instance, providerFactory, UserFederationInstances) {
+module.controller('GenericUserFederationCtrl', function($scope, $location, Notifications, Dialog, realm, instance, providerFactory, UserFederationInstances, UserFederationSync) {
     console.log('GenericUserFederationCtrl');
 
-    $scope.instance = angular.copy(instance);
     $scope.create = !instance.providerName;
     $scope.providerFactory = providerFactory;
 
     console.log("providerFactory: " + providerFactory.id);
 
-    if ($scope.create) {
-        $scope.instance.providerName = providerFactory.id;
-        $scope.instance.config = {};
-        $scope.instance.priority = 0;
+    function initFederationSettings() {
+        if ($scope.create) {
+            instance.providerName = providerFactory.id;
+            instance.config = {};
+            instance.priority = 0;
+            $scope.fullSyncEnabled = false;
+            $scope.changedSyncEnabled = false;
+        } else {
+            $scope.fullSyncEnabled = (instance.fullSyncPeriod && instance.fullSyncPeriod > 0);
+            $scope.changedSyncEnabled = (instance.changedSyncPeriod && instance.changedSyncPeriod > 0);
+        }
+
+        $scope.changed = false;
     }
 
+    initFederationSettings();
+    $scope.instance = angular.copy(instance);
     $scope.realm = realm;
 
+    $scope.$watch('fullSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
+
+        $scope.instance.fullSyncPeriod = $scope.fullSyncEnabled ? 604800 : -1;
+        $scope.changed = true;
+    });
+
+    $scope.$watch('changedSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
 
-    $scope.changed = false;
+        $scope.instance.changedSyncPeriod = $scope.changedSyncEnabled ? 86400 : -1;
+        $scope.changed = true;
+    });
 
     $scope.$watch('instance', function() {
         if (!angular.equals($scope.instance, instance)) {
@@ -405,13 +430,8 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
     };
 
     $scope.reset = function() {
+        initFederationSettings();
         $scope.instance = angular.copy(instance);
-        if ($scope.create) {
-            $scope.instance.providerName = providerFactory.id;
-            $scope.instance.config = {};
-            $scope.instance.priority = 0;
-        }
-        $scope.changed = false;
     };
 
     $scope.cancel = function() {
@@ -430,37 +450,70 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
         });
     };
 
+    $scope.triggerFullSync = function() {
+        console.log('GenericCtrl: triggerFullSync');
+        triggerSync('triggerFullSync');
+    }
+
+    $scope.triggerChangedUsersSync = function() {
+        console.log('GenericCtrl: triggerChangedUsersSync');
+        triggerSync('triggerChangedUsersSync');
+    }
 
+    function triggerSync(action) {
+        UserFederationSync.get({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, function() {
+            Notifications.success("Sync of users finished successfully");
+        }, function() {
+            Notifications.error("Error during sync of users");
+        });
+    }
 });
 
 
-module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog, realm, instance, UserFederationInstances, RealmLDAPConnectionTester) {
+module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog, realm, instance, UserFederationInstances, UserFederationSync, RealmLDAPConnectionTester) {
     console.log('LDAPCtrl');
+    var DEFAULT_BATCH_SIZE = "1000";
 
-    $scope.instance = angular.copy(instance);
     $scope.create = !instance.providerName;
 
-    if ($scope.create) {
-        $scope.instance.providerName = "ldap";
-        $scope.instance.config = {};
-        $scope.instance.priority = 0;
-        $scope.syncRegistrations = false;
+    function initFederationSettings() {
+        if ($scope.create) {
+            instance.providerName = "ldap";
+            instance.config = {};
+            instance.priority = 0;
+            $scope.syncRegistrations = false;
 
-        $scope.userAccountControlsAfterPasswordUpdate = true;
-        $scope.instance.config.userAccountControlsAfterPasswordUpdate = "true";
+            $scope.userAccountControlsAfterPasswordUpdate = true;
+            instance.config.userAccountControlsAfterPasswordUpdate = "true";
 
-        $scope.connectionPooling = true;
-        $scope.instance.config.connectionPooling = "true";
+            $scope.connectionPooling = true;
+            instance.config.connectionPooling = "true";
 
-        $scope.pagination = true;
-        $scope.instance.config.pagination = "true";
-    } else {
-        $scope.syncRegistrations = instance.config.syncRegistrations && instance.config.syncRegistrations == "true";
-        $scope.userAccountControlsAfterPasswordUpdate = instance.config.userAccountControlsAfterPasswordUpdate && instance.config.userAccountControlsAfterPasswordUpdate == "true";
-        $scope.connectionPooling = instance.config.connectionPooling && instance.config.connectionPooling == "true";
-        $scope.pagination = instance.config.pagination && instance.config.pagination == "true";
+            $scope.pagination = true;
+            instance.config.pagination = "true";
+            instance.config.batchSizeForSync = DEFAULT_BATCH_SIZE;
+
+            $scope.fullSyncEnabled = false;
+            $scope.changedSyncEnabled = false;
+        } else {
+            $scope.syncRegistrations = instance.config.syncRegistrations && instance.config.syncRegistrations == "true";
+            $scope.userAccountControlsAfterPasswordUpdate = instance.config.userAccountControlsAfterPasswordUpdate && instance.config.userAccountControlsAfterPasswordUpdate == "true";
+            $scope.connectionPooling = instance.config.connectionPooling && instance.config.connectionPooling == "true";
+            $scope.pagination = instance.config.pagination && instance.config.pagination == "true";
+            if (!instance.config.batchSizeForSync) {
+                instance.config.batchSizeForSync = DEFAULT_BATCH_SIZE;
+            }
+            $scope.fullSyncEnabled = (instance.fullSyncPeriod && instance.fullSyncPeriod > 0);
+            $scope.changedSyncEnabled = (instance.changedSyncPeriod && instance.changedSyncPeriod > 0);
+        }
+
+        $scope.changed = false;
+        $scope.lastVendor = instance.config.vendor;
     }
 
+    initFederationSettings();
+    $scope.instance = angular.copy(instance);
+
     $scope.ldapVendors = [
         { "id": "ad", "name": "Active Directory" },
         { "id": "rhds", "name": "Red Hat Directory Server" },
@@ -473,11 +526,6 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog,
 
     $scope.realm = realm;
 
-
-    $scope.changed = false;
-
-    $scope.lastVendor = $scope.instance.config.vendor;
-
     function watchBooleanProperty(propertyName) {
         $scope.$watch(propertyName, function() {
             if ($scope[propertyName]) {
@@ -493,6 +541,24 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog,
     watchBooleanProperty('connectionPooling');
     watchBooleanProperty('pagination');
 
+    $scope.$watch('fullSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
+
+        $scope.instance.fullSyncPeriod = $scope.fullSyncEnabled ? 604800 : -1;
+        $scope.changed = true;
+    });
+
+    $scope.$watch('changedSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
+
+        $scope.instance.changedSyncPeriod = $scope.changedSyncEnabled ? 86400 : -1;
+        $scope.changed = true;
+    });
+
     $scope.$watch('instance', function() {
         if (!angular.equals($scope.instance, instance)) {
             $scope.changed = true;
@@ -514,6 +580,13 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog,
 
     $scope.save = function() {
         $scope.changed = false;
+
+        if (!parseInt($scope.instance.config.batchSizeForSync)) {
+            $scope.instance.config.batchSizeForSync = DEFAULT_BATCH_SIZE;
+        } else {
+            $scope.instance.config.batchSizeForSync = parseInt($scope.instance.config.batchSizeForSync).toString();
+        }
+
         if ($scope.create) {
             UserFederationInstances.save({realm: realm.realm}, $scope.instance,  function () {
                 $scope.changed = false;
@@ -534,15 +607,8 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog,
     };
 
     $scope.reset = function() {
+        initFederationSettings();
         $scope.instance = angular.copy(instance);
-        if ($scope.create) {
-            $scope.instance.providerName = "ldap";
-            $scope.instance.config = {};
-            $scope.instance.priority = 0;
-            $scope.syncRegistrations = false;
-        }
-        $scope.changed = false;
-        $scope.lastVendor = $scope.instance.config.vendor;
     };
 
     $scope.cancel = function() {
@@ -589,5 +655,24 @@ module.controller('LDAPCtrl', function($scope, $location, Notifications, Dialog,
             Notifications.error("LDAP authentication failed. See server.log for details");
         });
     }
+
+    $scope.triggerFullSync = function() {
+        console.log('LDAPCtrl: triggerFullSync');
+        triggerSync('triggerFullSync');
+    }
+
+    $scope.triggerChangedUsersSync = function() {
+        console.log('LDAPCtrl: triggerChangedUsersSync');
+        triggerSync('triggerChangedUsersSync');
+    }
+
+    function triggerSync(action) {
+        UserFederationSync.get({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, function() {
+            Notifications.success("Sync of users finished successfully");
+        }, function() {
+            Notifications.error("Error during sync of users");
+        });
+    }
+
 });
 
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js
index e8af664..efed6c7 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js
@@ -217,6 +217,10 @@ module.factory('UserFederationProviders', function($resource) {
     });
 });
 
+module.factory('UserFederationSync', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/user-federation/sync/:provider');
+});
+
 
 module.factory('UserSessionStats', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/users/:user/session-stats', {
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-generic.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-generic.html
index b046180..7dcae0f 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-generic.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-generic.html
@@ -40,6 +40,34 @@
 
             </fieldset>
 
+            <fieldset>
+                <legend><span class="text">Sync settings</span></legend>
+                <div class="form-group clearfix">
+                    <label class="col-sm-2 control-label" for="fullSyncEnabled">Periodic full sync</label>
+                    <div class="col-sm-4">
+                        <input ng-model="fullSyncEnabled" name="fullSyncEnabled" id="fullSyncEnabled" onoffswitch />
+                    </div>
+                </div>
+                <div class="form-group clearfix" data-ng-show="fullSyncEnabled">
+                    <label class="col-sm-2 control-label" for="fullSyncPeriod">Full sync period</label>
+                    <div class="col-sm-4">
+                        <input class="form-control" type="number" ng-model="instance.fullSyncPeriod" id="fullSyncPeriod" />
+                    </div>
+                </div>
+                <div class="form-group clearfix">
+                    <label class="col-sm-2 control-label" for="changedSyncEnabled">Periodic changed users sync</label>
+                    <div class="col-sm-4">
+                        <input ng-model="changedSyncEnabled" name="changedSyncEnabled" id="changedSyncEnabled" onoffswitch />
+                    </div>
+                </div>
+                <div class="form-group clearfix" data-ng-show="changedSyncEnabled">
+                    <label class="col-sm-2 control-label" for="changedSyncPeriod">Changed users sync period</label>
+                    <div class="col-sm-4">
+                        <input class="form-control" type="number" ng-model="instance.changedSyncPeriod" id="changedSyncPeriod" />
+                    </div>
+                </div>
+            </fieldset>
+
             <div class="pull-right form-actions" data-ng-show="create && access.manageUsers">
                 <button kc-cancel data-ng-click="cancel()">Cancel</button>
                 <button kc-save data-ng-show="changed">Save</button>
@@ -49,6 +77,8 @@
                 <button kc-reset data-ng-show="changed">Clear changes</button>
                 <button kc-save  data-ng-show="changed">Save</button>
                 <button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete</button>
+                <button kc-delete data-ng-click="triggerFullSync()" data-ng-hide="changed">Synchronize all users</button>
+                <button kc-delete data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed">Synchronize changed users</button>
             </div>
         </form>
     </div>
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html
index c9dd366..da1e04c 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html
@@ -133,6 +133,40 @@
                 </div>
             </fieldset>
 
+            <fieldset>
+                <legend><span class="text">Sync settings</span></legend>
+                <div class="form-group clearfix">
+                    <label class="col-sm-2 control-label" for="batchSizeForSync">Batch size</label>
+                    <div class="col-sm-4">
+                        <input class="form-control" type="text" ng-model="instance.config.batchSizeForSync" id="batchSizeForSync" />
+                    </div>
+                </div>
+                <div class="form-group clearfix">
+                    <label class="col-sm-2 control-label" for="fullSyncEnabled">Periodic full sync</label>
+                    <div class="col-sm-4">
+                        <input ng-model="fullSyncEnabled" name="fullSyncEnabled" id="fullSyncEnabled" onoffswitch />
+                    </div>
+                </div>
+                <div class="form-group clearfix" data-ng-show="fullSyncEnabled">
+                    <label class="col-sm-2 control-label" for="fullSyncPeriod">Full sync period</label>
+                    <div class="col-sm-4">
+                        <input class="form-control" type="number" ng-model="instance.fullSyncPeriod" id="fullSyncPeriod" />
+                    </div>
+                </div>
+                <div class="form-group clearfix">
+                    <label class="col-sm-2 control-label" for="changedSyncEnabled">Periodic changed users sync</label>
+                    <div class="col-sm-4">
+                        <input ng-model="changedSyncEnabled" name="changedSyncEnabled" id="changedSyncEnabled" onoffswitch />
+                    </div>
+                </div>
+                <div class="form-group clearfix" data-ng-show="changedSyncEnabled">
+                    <label class="col-sm-2 control-label" for="changedSyncPeriod">Changed users sync period</label>
+                    <div class="col-sm-4">
+                        <input class="form-control" type="number" ng-model="instance.changedSyncPeriod" id="changedSyncPeriod" />
+                    </div>
+                </div>
+            </fieldset>
+
             <div class="pull-right form-actions" data-ng-show="create && access.manageUsers">
                 <button kc-cancel data-ng-click="cancel()">Cancel</button>
                 <button kc-save data-ng-show="changed">Save</button>
@@ -142,6 +176,8 @@
                 <button kc-reset data-ng-show="changed">Clear changes</button>
                 <button kc-save  data-ng-show="changed">Save</button>
                 <button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete</button>
+                <button kc-delete data-ng-click="triggerFullSync()" data-ng-hide="changed">Synchronize all users</button>
+                <button kc-delete data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed">Synchronize changed users</button>
             </div>
         </form>
     </div>
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java
index 607a10c..a1d232e 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java
@@ -122,12 +122,14 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
                     cache.clear();
                 }
                 runInvalidations();
+                transactionActive = false;
             }
 
             @Override
             public void rollback() {
                 setRollbackOnly = true;
                 runInvalidations();
+                transactionActive = false;
             }
 
             @Override
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
index 781a3de..678bf70 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java
@@ -87,12 +87,14 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
                     cache.clear();
                 }
                 runInvalidations();
+                transactionActive = false;
             }
 
             @Override
             public void rollback() {
                 setRollbackOnly = true;
                 runInvalidations();
+                transactionActive = false;
             }
 
             @Override
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 c6ccb76..3574508 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -144,9 +144,9 @@ public class RealmManager {
             }
 
             // Remove all periodic syncs for configured federation providers
-            PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager();
+            UsersSyncManager usersSyncManager = new UsersSyncManager();
             for (final UserFederationProviderModel fedProvider : federationProviders) {
-                periodicSyncManager.removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), fedProvider);
+                usersSyncManager.removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), fedProvider);
             }
         }
         return removed;
@@ -218,9 +218,9 @@ public class RealmManager {
 
         // Refresh periodic sync tasks for configured federationProviders
         List<UserFederationProviderModel> federationProviders = newRealm.getUserFederationProviders();
-        PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager();
+        UsersSyncManager usersSyncManager = new UsersSyncManager();
         for (final UserFederationProviderModel fedProvider : federationProviders) {
-            periodicSyncManager.startPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, newRealm.getId());
+            usersSyncManager.refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, newRealm.getId());
         }
     }
 
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 b6af7a4..7b41b55 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -22,7 +22,7 @@ import org.keycloak.representations.adapters.action.SessionStats;
 import org.keycloak.representations.idm.RealmAuditRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.managers.LDAPConnectionTestManager;
-import org.keycloak.services.managers.PeriodicSyncManager;
+import org.keycloak.services.managers.UsersSyncManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.managers.ResourceAdminManager;
 import org.keycloak.services.managers.TokenManager;
@@ -165,9 +165,9 @@ public class RealmAdminResource {
 
             // Refresh periodic sync tasks for configured federationProviders
             List<UserFederationProviderModel> federationProviders = realm.getUserFederationProviders();
-            PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager();
+            UsersSyncManager usersSyncManager = new UsersSyncManager();
             for (final UserFederationProviderModel fedProvider : federationProviders) {
-                periodicSyncManager.startPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId());
+                usersSyncManager.refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId());
             }
 
             return Response.noContent().build();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java
index 6039ccb..554ed7d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java
@@ -12,7 +12,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation;
 import org.keycloak.representations.idm.UserFederationProviderRepresentation;
-import org.keycloak.services.managers.PeriodicSyncManager;
+import org.keycloak.services.managers.UsersSyncManager;
 import org.keycloak.timer.TimerProvider;
 
 import javax.ws.rs.Consumes;
@@ -23,6 +23,7 @@ import javax.ws.rs.PUT;
 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.Response;
 import javax.ws.rs.core.UriInfo;
@@ -120,7 +121,7 @@ public class UserFederationResource {
         }
         UserFederationProviderModel model = realm.addUserFederationProvider(rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName,
                 rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync());
-        new PeriodicSyncManager().startPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId());
+        new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId());
 
         return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
     }
@@ -144,7 +145,7 @@ public class UserFederationResource {
         UserFederationProviderModel model = new UserFederationProviderModel(id, rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName,
                 rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync());
         realm.updateUserFederationProvider(model);
-        new PeriodicSyncManager().startPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId());
+        new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId());
     }
 
     /**
@@ -179,7 +180,7 @@ public class UserFederationResource {
         auth.requireManage();
         UserFederationProviderModel model = new UserFederationProviderModel(id, null, null, -1, null, -1, -1, 0);
         realm.removeUserFederationProvider(model);
-        new PeriodicSyncManager().removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), model);
+        new UsersSyncManager().removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), model);
     }
 
 
@@ -202,5 +203,32 @@ public class UserFederationResource {
         return reps;
     }
 
+    /**
+     * trigger sync of users
+     *
+     * @return
+     */
+    @GET
+    @Path("sync/{id}")
+    @NoCache
+    public Response syncUsers(@PathParam("id") String providerId, @QueryParam("action") String action) {
+        logger.info("triggerSync");
+        auth.requireManage();
+
+        for (UserFederationProviderModel model : realm.getUserFederationProviders()) {
+            if (model.getId().equals(providerId)) {
+                UsersSyncManager syncManager = new UsersSyncManager();
+                if ("triggerFullSync".equals(action)) {
+                    syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), model);
+                } else if ("triggerChangedUsersSync".equals(action)) {
+                    syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), model);
+                }
+                return Response.noContent().build();
+            }
+        }
+
+        throw new NotFoundException("could not find provider");
+    }
+
 
 }
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 27f9a2f..724ebe6 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -15,7 +15,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.DefaultKeycloakSessionFactory;
 import org.keycloak.services.managers.ApplianceBootstrap;
 import org.keycloak.services.managers.BruteForceProtector;
-import org.keycloak.services.managers.PeriodicSyncManager;
+import org.keycloak.services.managers.UsersSyncManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.managers.TokenManager;
 import org.keycloak.services.resources.admin.AdminRoot;
@@ -149,7 +149,7 @@ public class KeycloakApplication extends Application {
         TimerProvider timer = sessionFactory.create().getProvider(TimerProvider.class);
         timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredAuditEvents()), interval, "ClearExpiredAuditEvents");
         timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
-        new PeriodicSyncManager().bootstrap(sessionFactory, timer);
+        new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer);
     }
 
     public KeycloakSessionFactory getSessionFactory() {
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
index 21ac290..f4dac01 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java
@@ -11,7 +11,6 @@ import org.junit.Test;
 import org.junit.rules.RuleChain;
 import org.junit.rules.TestRule;
 import org.junit.runners.MethodSorters;
-import org.keycloak.examples.federation.properties.ClasspathPropertiesFederationFactory;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
 import org.keycloak.federation.ldap.LDAPUtils;
@@ -23,11 +22,10 @@ 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.PeriodicSyncManager;
+import org.keycloak.services.managers.UsersSyncManager;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.rule.KeycloakRule;
 import org.keycloak.testsuite.rule.LDAPRule;
-import org.keycloak.testutils.DummyUserFederationProvider;
 import org.keycloak.testutils.DummyUserFederationProviderFactory;
 import org.keycloak.testutils.LDAPEmbeddedServer;
 import org.keycloak.timer.TimerProvider;
@@ -84,29 +82,43 @@ public class SyncProvidersTest {
 
     @Test
     public void testLDAPSync() {
+        UsersSyncManager usersSyncManager = new UsersSyncManager();
+
+        // wait a bit
+        sleep(1000);
+
         KeycloakSession session = keycloakRule.startSession();
         try {
             KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
-            UserFederationProviderFactory ldapFedFactory = (UserFederationProviderFactory)sessionFactory.getProviderFactory(UserFederationProvider.class, LDAPFederationProviderFactory.PROVIDER_NAME);
-            ldapFedFactory.syncAllUsers(sessionFactory, "test", ldapModel);
+            usersSyncManager.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();
+            // Assert users imported
             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");
 
+            // Assert lastSync time updated
+            Assert.assertTrue(ldapModel.getLastSync() > 0);
+            for (UserFederationProviderModel persistentFedModel : testRealm.getUserFederationProviders()) {
+                if (LDAPFederationProviderFactory.PROVIDER_NAME.equals(persistentFedModel.getProviderName())) {
+                    Assert.assertTrue(persistentFedModel.getLastSync() > 0);
+                } else {
+                    // Dummy provider has still 0
+                    Assert.assertEquals(0, persistentFedModel.getLastSync());
+                }
+            }
+
             // wait a bit
             sleep(1000);
-            Date beforeLDAPUpdate = new Date();
 
             // Add user to LDAP and update 'user5' in LDAP
             PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(session, ldapModel);
@@ -119,8 +131,7 @@ public class SyncProvidersTest {
 
             // Trigger partial sync
             KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
-            UserFederationProviderFactory ldapFedFactory = (UserFederationProviderFactory)sessionFactory.getProviderFactory(UserFederationProvider.class, LDAPFederationProviderFactory.PROVIDER_NAME);
-            ldapFedFactory.syncChangedUsers(sessionFactory, "test", ldapModel, beforeLDAPUpdate);
+            usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel);
         } finally {
             keycloakRule.stopSession(session, false);
         }
@@ -147,12 +158,12 @@ public class SyncProvidersTest {
             int changed = dummyFedFactory.getChangedSyncCounter();
 
             // Assert that after some period was DummyUserFederationProvider triggered
-            PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager();
-            periodicSyncManager.bootstrap(sessionFactory, session.getProvider(TimerProvider.class));
+            UsersSyncManager usersSyncManager = new UsersSyncManager();
+            usersSyncManager.bootstrapPeriodic(sessionFactory, session.getProvider(TimerProvider.class));
             sleep(1800);
 
             // Cancel timer
-            periodicSyncManager.removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), dummyModel);
+            usersSyncManager.removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), dummyModel);
 
             // Assert that DummyUserFederationProviderFactory.syncChangedUsers was invoked
             int newChanged = dummyFedFactory.getChangedSyncCounter();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/TransactionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/TransactionsTest.java
new file mode 100644
index 0000000..c2c5da4
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/TransactionsTest.java
@@ -0,0 +1,32 @@
+package org.keycloak.testsuite.model;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.testsuite.rule.KeycloakRule;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class TransactionsTest {
+
+    @ClassRule
+    public static KeycloakRule kc = new KeycloakRule();
+
+    @Test
+    public void testTransactionActive() {
+        KeycloakSession session = kc.startSession();
+
+        Assert.assertTrue(session.getTransaction().isActive());
+        session.getTransaction().commit();
+        Assert.assertFalse(session.getTransaction().isActive());
+
+        session.getTransaction().begin();
+        Assert.assertTrue(session.getTransaction().isActive());
+        session.getTransaction().rollback();
+        Assert.assertFalse(session.getTransaction().isActive());
+
+        session.close();
+    }
+}