From f0b66a2241f396e57b47dcff7967a68339a727c8 Mon Sep 17 00:00:00 2001 From: mposolda Date: Mon, 11 Aug 2014 22:25:58 +0200 Subject: [PATCH 1/4] Admin console UI for sync of users from LDAP. Sync fixes --- .../base/resources/js/controllers/users.js | 111 +++++++++++++----- .../theme/admin/base/resources/js/services.js | 4 + .../resources/partials/federated-ldap.html | 36 ++++++ .../services/managers/RealmManager.java | 8 +- ...SyncManager.java => UsersSyncManager.java} | 44 +++++-- .../resources/KeycloakApplication.java | 4 +- .../resources/admin/RealmAdminResource.java | 6 +- .../admin/UserFederationResource.java | 36 +++++- .../testsuite/forms/SyncProvidersTest.java | 35 ++++-- 9 files changed, 218 insertions(+), 66 deletions(-) rename services/src/main/java/org/keycloak/services/managers/{PeriodicSyncManager.java => UsersSyncManager.java} (63%) 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 706cd0ed32..d5f2e2ce70 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 @@ -434,33 +434,50 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif }); -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 +490,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 +505,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 +544,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 +571,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 +619,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 e8af6643ec..efed6c785a 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-ldap.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html index c9dd366495..49fb638339 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 @@ +
+ Sync settings +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
@@ -142,6 +176,8 @@ + +
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 c6ccb76074..3574508e55 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 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/managers/PeriodicSyncManager.java b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java similarity index 63% rename from services/src/main/java/org/keycloak/services/managers/PeriodicSyncManager.java rename to services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java index 7a86529899..e5bd2e6ff5 100644 --- a/services/src/main/java/org/keycloak/services/managers/PeriodicSyncManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UsersSyncManager.java @@ -2,6 +2,7 @@ package org.keycloak.services.managers; import java.util.List; +import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionTask; @@ -16,7 +17,9 @@ import org.keycloak.util.Time; /** * @author Marek Posolda */ -public class PeriodicSyncManager { +public class UsersSyncManager { + + protected static final Logger logger = Logger.getLogger(UsersSyncManager.class); /** * Check federationProviderModel of all realms and possibly start periodic sync for them @@ -24,7 +27,7 @@ public class PeriodicSyncManager { * @param sessionFactory * @param timer */ - public void bootstrap(final KeycloakSessionFactory sessionFactory, final TimerProvider timer) { + public void bootstrapPeriodic(final KeycloakSessionFactory sessionFactory, final TimerProvider timer) { KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { @Override @@ -33,27 +36,45 @@ public class PeriodicSyncManager { for (final RealmModel realm : realms) { List federationProviders = realm.getUserFederationProviders(); for (final UserFederationProviderModel fedProvider : federationProviders) { - startPeriodicSyncForProvider(sessionFactory, timer, fedProvider, realm.getId()); + refreshPeriodicSyncForProvider(sessionFactory, timer, fedProvider, realm.getId()); } } } }); } - public void startPeriodicSyncForProvider(final KeycloakSessionFactory sessionFactory, TimerProvider timer, final UserFederationProviderModel fedProvider, final String realmId) { + public void syncAllUsers(final KeycloakSessionFactory sessionFactory, String realmId, final UserFederationProviderModel fedProvider) { + final UserFederationProviderFactory fedProviderFactory = (UserFederationProviderFactory) sessionFactory.getProviderFactory(UserFederationProvider.class, fedProvider.getProviderName()); + updateLastSyncInterval(sessionFactory, fedProvider, realmId); + fedProviderFactory.syncAllUsers(sessionFactory, realmId, fedProvider); + } + + public void syncChangedUsers(final KeycloakSessionFactory sessionFactory, String realmId, final UserFederationProviderModel fedProvider) { final UserFederationProviderFactory fedProviderFactory = (UserFederationProviderFactory) sessionFactory.getProviderFactory(UserFederationProvider.class, fedProvider.getProviderName()); + // See when we did last sync. + int oldLastSync = fedProvider.getLastSync(); + updateLastSyncInterval(sessionFactory, fedProvider, realmId); + fedProviderFactory.syncChangedUsers(sessionFactory, realmId, fedProvider, Time.toDate(oldLastSync)); + } + + public void refreshPeriodicSyncForProvider(final KeycloakSessionFactory sessionFactory, TimerProvider timer, final UserFederationProviderModel fedProvider, final String realmId) { if (fedProvider.getFullSyncPeriod() > 0) { // We want periodic full sync for this provider timer.schedule(new Runnable() { @Override public void run() { - updateLastSyncInterval(sessionFactory, fedProvider, realmId); - fedProviderFactory.syncAllUsers(sessionFactory, realmId, fedProvider); + try { + syncAllUsers(sessionFactory, realmId, fedProvider); + } catch (Throwable t) { + logger.error("Error occured during full sync of users", t); + } } }, fedProvider.getFullSyncPeriod() * 1000, fedProvider.getId() + "-FULL"); + } else { + timer.cancelTask(fedProvider.getId() + "-FULL"); } if (fedProvider.getChangedSyncPeriod() > 0) { @@ -62,14 +83,17 @@ public class PeriodicSyncManager { @Override public void run() { - // See when we did last sync. - int oldLastSync = fedProvider.getLastSync(); - updateLastSyncInterval(sessionFactory, fedProvider, realmId); - fedProviderFactory.syncChangedUsers(sessionFactory, realmId, fedProvider, Time.toDate(oldLastSync)); + try { + syncChangedUsers(sessionFactory, realmId, fedProvider); + } catch (Throwable t) { + logger.error("Error occured during sync of changed users", t); + } } }, fedProvider.getChangedSyncPeriod() * 1000, fedProvider.getId() + "-CHANGED"); + } else { + timer.cancelTask(fedProvider.getId() + "-CHANGED"); } } 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 27f9a2fdaa..724ebe6531 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/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index b6af7a46e9..7b41b55ade 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 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 6039ccbade..1669123170 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 getUserFederationInstances(@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/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java index 21ac2901fa..f4dac01906 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(); From 4bac0474c603ace0c1c2be92fab0f83543d6e6bc Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 12 Aug 2014 18:10:25 +0200 Subject: [PATCH 2/4] Sync UI for generic providers --- .../BasePropertiesFederationFactory.java | 22 ++----- .../base/resources/js/controllers/users.js | 60 +++++++++++++++---- .../resources/partials/federated-generic.html | 30 ++++++++++ .../resources/partials/federated-ldap.html | 4 +- .../admin/UserFederationResource.java | 2 +- 5 files changed, 86 insertions(+), 32 deletions(-) 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 b0d9925c37..d79a13b743 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 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 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 d5f2e2ce70..46674ecf32 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 @@ -359,22 +359,47 @@ module.controller('UserFederationCtrl', function($scope, $location, realm, UserF module.controller('GenericUserFederationCtrl', function($scope, $location, Notifications, Dialog, realm, instance, providerFactory, UserFederationInstances) { 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.changed = false; + $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)) { @@ -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,7 +450,23 @@ 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"); + }); + } }); 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 b046180a89..7dcae0fe77 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 @@ +
+ Sync settings +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
@@ -49,6 +77,8 @@ + +
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 49fb638339..da1e04ca5d 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 @@ -176,8 +176,8 @@ - - + + 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 1669123170..554ed7d6f0 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 @@ -211,7 +211,7 @@ public class UserFederationResource { @GET @Path("sync/{id}") @NoCache - public Response getUserFederationInstances(@PathParam("id") String providerId, @QueryParam("action") String action) { + public Response syncUsers(@PathParam("id") String providerId, @QueryParam("action") String action) { logger.info("triggerSync"); auth.requireManage(); From 8419e5883dcd671e28c47553164ba325170a4da4 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 12 Aug 2014 18:58:37 +0200 Subject: [PATCH 3/4] Ensure that transaction.isActive() is false after commit or rollback --- .../mongo/MongoKeycloakTransaction.java | 2 ++ .../cache/DefaultCacheRealmProvider.java | 2 ++ .../cache/DefaultCacheUserProvider.java | 2 ++ .../testsuite/model/TransactionsTest.java | 32 +++++++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/model/TransactionsTest.java 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 705b88846e..a8dffc7260 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/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 607a10ccaf..a1d232ecb3 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 781a3de382..678bf70ae7 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/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 0000000000..c2c5da4bfe --- /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 Marek Posolda + */ +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(); + } +} From 687aa78567389064bd49b0e2046d11352afd4abd Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 12 Aug 2014 19:12:05 +0200 Subject: [PATCH 4/4] Fix issue in sync admin UI --- .../theme/admin/base/resources/js/controllers/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 46674ecf32..52a933f90a 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,7 +356,7 @@ 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.create = !instance.providerName;