diff --git a/core/src/main/java/org/keycloak/representations/idm/UserFederationProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserFederationProviderRepresentation.java index 19f1d0c6cf..7229435056 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserFederationProviderRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserFederationProviderRepresentation.java @@ -12,6 +12,9 @@ public class UserFederationProviderRepresentation { private String providerName; private Map config; private int priority; + private int fullSyncPeriod; + private int changedSyncPeriod; + private int lastSync; public String getId() { return id; @@ -54,6 +57,30 @@ public class UserFederationProviderRepresentation { this.priority = priority; } + public int getFullSyncPeriod() { + return fullSyncPeriod; + } + + public void setFullSyncPeriod(int fullSyncPeriod) { + this.fullSyncPeriod = fullSyncPeriod; + } + + public int getChangedSyncPeriod() { + return changedSyncPeriod; + } + + public void setChangedSyncPeriod(int changedSyncPeriod) { + this.changedSyncPeriod = changedSyncPeriod; + } + + public int getLastSync() { + return lastSync; + } + + public void setLastSync(int lastSync) { + this.lastSync = lastSync; + } + @Override public boolean equals(Object o) { if (this == o) return true; 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 04845fb2b3..b0d9925c37 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 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); + + 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/ClasspathPropertiesFederationFactory.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationFactory.java index f079e8c08d..f06250ac35 100755 --- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationFactory.java +++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationFactory.java @@ -12,6 +12,8 @@ import java.util.Properties; */ public class ClasspathPropertiesFederationFactory extends BasePropertiesFederationFactory { + public static final String PROVIDER_NAME = "classpath-properties"; + @Override protected BasePropertiesFederationProvider createProvider(KeycloakSession session, UserFederationProviderModel model, Properties props) { return new ClasspathPropertiesFederationProvider(session, model, props); @@ -30,6 +32,6 @@ public class ClasspathPropertiesFederationFactory extends BasePropertiesFederati @Override public String getId() { - return "classpath-properties"; + return PROVIDER_NAME; } } 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 5c64b73316..2f8957552c 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/ExportImportJob.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportJob.java deleted file mode 100755 index 6b987996d2..0000000000 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportJob.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.keycloak.exportimport.util; - -import org.keycloak.models.KeycloakSession; - -import java.io.IOException; - -/** - * Task to be executed inside transaction - * - * @author Marek Posolda - */ -public interface ExportImportJob { - - public void run(KeycloakSession session) throws IOException; -} 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 0000000000..185a9663ec --- /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 Marek Posolda + */ +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/ExportImportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportUtils.java deleted file mode 100755 index 506124af49..0000000000 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportImportUtils.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.keycloak.exportimport.util; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.KeycloakTransaction; -import org.keycloak.models.RealmModel; - -import java.io.IOException; - -/** - * @author Marek Posolda - */ -public class ExportImportUtils { - - /** - * Wrap given runnable job into KeycloakTransaction. - * - * @param factory - * @param job - */ - public static void runJobInTransaction(KeycloakSessionFactory factory, ExportImportJob job) throws IOException { - KeycloakSession session = factory.create(); - KeycloakTransaction tx = session.getTransaction(); - try { - tx.begin(); - job.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/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 aa4d83dcf2..edc5b52015 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 5899757c6b..2f66d09b42 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 4c532f05b4..739957470c 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 17cec51798..a8d7d98937 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 realms = session.realms().getRealms(); List reps = new ArrayList(); 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 6fbe1c8a36..a1e1109ca4 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 Marek Posolda @@ -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 d9a93ee49d..69cfd461d2 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 fed574b1e6..85a049ff1c 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 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 53aeb0ca17..88bf66f826 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 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 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 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 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 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 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 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 d67c499986..f862d03ef2 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/KeycloakSessionTask.java b/model/api/src/main/java/org/keycloak/models/KeycloakSessionTask.java new file mode 100644 index 0000000000..976ad0896c --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/KeycloakSessionTask.java @@ -0,0 +1,12 @@ +package org.keycloak.models; + +/** + * Task to be executed inside transaction + * + * @author Marek Posolda + */ +public interface KeycloakSessionTask { + + public void run(KeycloakSession session); + +} 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 1328c011eb..38b538e5d0 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/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 633756f731..4ae280f76b 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -161,7 +161,7 @@ public interface RealmModel extends RoleContainerModel { List getUserFederationProviders(); - UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName); + UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName, int fullSyncPeriod, int changedSyncPeriod, int lastSync); void updateUserFederationProvider(UserFederationProviderModel provider); void removeUserFederationProvider(UserFederationProviderModel provider); void setUserFederationProviders(List providers); 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 956779fd11..424764988d 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 config = new HashMap(); private int priority; private String displayName; + private int fullSyncPeriod = -1; // In seconds. -1 means that periodic full sync is disabled + private int changedSyncPeriod = -1; // In seconds. -1 means that periodic changed sync is disabled + private int lastSync; // Date when last sync was done for this provider public UserFederationProviderModel() {}; - public UserFederationProviderModel(String id, String providerName, Map config, int priority, String displayName) { + public UserFederationProviderModel(String id, String providerName, Map config, int priority, String displayName, int fullSyncPeriod, int changedSyncPeriod, int lastSync) { this.id = id; this.providerName = providerName; if (config != null) { @@ -27,6 +30,9 @@ public class UserFederationProviderModel { } this.priority = priority; this.displayName = displayName; + this.fullSyncPeriod = fullSyncPeriod; + this.changedSyncPeriod = changedSyncPeriod; + this.lastSync = lastSync; } public String getId() { @@ -64,4 +70,28 @@ public class UserFederationProviderModel { public void setDisplayName(String displayName) { this.displayName = displayName; } + + public int getFullSyncPeriod() { + return fullSyncPeriod; + } + + public void setFullSyncPeriod(int fullSyncPeriod) { + this.fullSyncPeriod = fullSyncPeriod; + } + + public int getChangedSyncPeriod() { + return changedSyncPeriod; + } + + public void setChangedSyncPeriod(int changedSyncPeriod) { + this.changedSyncPeriod = changedSyncPeriod; + } + + public int getLastSync() { + return lastSync; + } + + public void setLastSync(int lastSync) { + this.lastSync = lastSync; + } } diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserFederationProviderEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserFederationProviderEntity.java index 6d3916e7e7..fe004651c8 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/UserFederationProviderEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/UserFederationProviderEntity.java @@ -12,6 +12,9 @@ public class UserFederationProviderEntity { protected Map config; protected int priority; protected String displayName; + private int fullSyncPeriod; + private int changedSyncPeriod; + private int lastSync; public String getId() { @@ -53,4 +56,28 @@ public class UserFederationProviderEntity { public void setDisplayName(String displayName) { this.displayName = displayName; } + + public int getFullSyncPeriod() { + return fullSyncPeriod; + } + + public void setFullSyncPeriod(int fullSyncPeriod) { + this.fullSyncPeriod = fullSyncPeriod; + } + + public int getChangedSyncPeriod() { + return changedSyncPeriod; + } + + public void setChangedSyncPeriod(int changedSyncPeriod) { + this.changedSyncPeriod = changedSyncPeriod; + } + + public int getLastSync() { + return lastSync; + } + + public void setLastSync(int lastSync) { + this.lastSync = 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 ca07fdb0e7..2f329b7858 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/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index ab3c865241..44994651b7 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -262,6 +262,9 @@ public class ModelToRepresentation { rep.setProviderName(model.getProviderName()); rep.setPriority(model.getPriority()); rep.setDisplayName(model.getDisplayName()); + rep.setFullSyncPeriod(model.getFullSyncPeriod()); + rep.setChangedSyncPeriod(model.getChangedSyncPeriod()); + rep.setLastSync(model.getLastSync()); return rep; } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 66c8be9b06..c7d7c263f7 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -288,7 +288,8 @@ public class RepresentationToModel { for (UserFederationProviderRepresentation representation : providers) { UserFederationProviderModel model = new UserFederationProviderModel(representation.getId(), representation.getProviderName(), - representation.getConfig(), representation.getPriority(), representation.getDisplayName()); + representation.getConfig(), representation.getPriority(), representation.getDisplayName(), + representation.getFullSyncPeriod(), representation.getChangedSyncPeriod(), representation.getLastSync()); result.add(model); } return result; diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index 5268441877..9fb8a1ee79 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -594,9 +594,9 @@ public class RealmAdapter implements RealmModel { } @Override - public UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName) { + public UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName, int fullSyncPeriod, int changedSyncPeriod, int lastSync) { getDelegateForUpdate(); - return updated.addUserFederationProvider(providerName, config, priority, displayName); + return updated.addUserFederationProvider(providerName, config, priority, displayName, fullSyncPeriod, changedSyncPeriod, lastSync); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index c53354ebba..4f0da3cbc2 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -676,14 +676,15 @@ public class RealmAdapter implements RealmModel { }); List result = new ArrayList(); for (UserFederationProviderEntity entity : copy) { - result.add(new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName())); + result.add(new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName(), + entity.getFullSyncPeriod(), entity.getChangedSyncPeriod(), entity.getLastSync())); } return result; } @Override - public UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName) { + public UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName, int fullSyncPeriod, int changedSyncPeriod, int lastSync) { String id = KeycloakModelUtils.generateId(); UserFederationProviderEntity entity = new UserFederationProviderEntity(); entity.setId(id); @@ -695,10 +696,13 @@ public class RealmAdapter implements RealmModel { displayName = id; } entity.setDisplayName(displayName); + entity.setFullSyncPeriod(fullSyncPeriod); + entity.setChangedSyncPeriod(changedSyncPeriod); + entity.setLastSync(lastSync); em.persist(entity); realm.getUserFederationProviders().add(entity); em.flush(); - return new UserFederationProviderModel(entity.getId(), providerName, config, priority, displayName); + return new UserFederationProviderModel(entity.getId(), providerName, config, priority, displayName, fullSyncPeriod, changedSyncPeriod, lastSync); } @Override @@ -728,6 +732,9 @@ public class RealmAdapter implements RealmModel { entity.setPriority(model.getPriority()); entity.setProviderName(model.getProviderName()); entity.setPriority(model.getPriority()); + entity.setFullSyncPeriod(model.getFullSyncPeriod()); + entity.setChangedSyncPeriod(model.getChangedSyncPeriod()); + entity.setLastSync(model.getLastSync()); break; } } @@ -750,13 +757,17 @@ public class RealmAdapter implements RealmModel { if (displayName != null) { entity.setDisplayName(model.getDisplayName()); } + entity.setFullSyncPeriod(model.getFullSyncPeriod()); + entity.setChangedSyncPeriod(model.getChangedSyncPeriod()); + entity.setLastSync(model.getLastSync()); found = true; break; } } if (found) continue; - session.users().preRemove(this, new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName())); + session.users().preRemove(this, new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName(), + entity.getFullSyncPeriod(), entity.getChangedSyncPeriod(), entity.getLastSync())); it.remove(); em.remove(entity); } @@ -786,6 +797,9 @@ public class RealmAdapter implements RealmModel { displayName = entity.getId(); } entity.setDisplayName(displayName); + entity.setFullSyncPeriod(model.getFullSyncPeriod()); + entity.setChangedSyncPeriod(model.getChangedSyncPeriod()); + entity.setLastSync(model.getLastSync()); em.persist(entity); realm.getUserFederationProviders().add(entity); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserFederationProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserFederationProviderEntity.java index aec144e6fc..22370f3c88 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserFederationProviderEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserFederationProviderEntity.java @@ -42,6 +42,13 @@ public class UserFederationProviderEntity { @Column(name="DISPLAY_NAME") private String displayName; + @Column(name="FULL_SYNC_PERIOD") + private int fullSyncPeriod; + @Column(name="CHANGED_SYNC_PERIOD") + private int changedSyncPeriod; + @Column(name="LAST_SYNC") + private int lastSync; + public String getId() { return id; } @@ -89,4 +96,28 @@ public class UserFederationProviderEntity { public void setDisplayName(String displayName) { this.displayName = displayName; } + + public int getFullSyncPeriod() { + return fullSyncPeriod; + } + + public void setFullSyncPeriod(int fullSyncPeriod) { + this.fullSyncPeriod = fullSyncPeriod; + } + + public int getChangedSyncPeriod() { + return changedSyncPeriod; + } + + public void setChangedSyncPeriod(int changedSyncPeriod) { + this.changedSyncPeriod = changedSyncPeriod; + } + + public int getLastSync() { + return lastSync; + } + + public void setLastSync(int lastSync) { + this.lastSync = lastSync; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index f275dee36b..893f52e0f9 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -757,7 +757,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme } @Override - public UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName) { + public UserFederationProviderModel addUserFederationProvider(String providerName, Map config, int priority, String displayName, int fullSyncPeriod, int changedSyncPeriod, int lastSync) { UserFederationProviderEntity entity = new UserFederationProviderEntity(); entity.setId(KeycloakModelUtils.generateId()); entity.setPriority(priority); @@ -767,10 +767,13 @@ public class RealmAdapter extends AbstractMongoAdapter impleme displayName = entity.getId(); } entity.setDisplayName(displayName); + entity.setFullSyncPeriod(fullSyncPeriod); + entity.setChangedSyncPeriod(changedSyncPeriod); + entity.setLastSync(lastSync); realm.getUserFederationProviders().add(entity); updateRealm(); - return new UserFederationProviderModel(entity.getId(), providerName, config, priority, displayName); + return new UserFederationProviderModel(entity.getId(), providerName, config, priority, displayName, fullSyncPeriod, changedSyncPeriod, lastSync); } @Override @@ -779,7 +782,8 @@ public class RealmAdapter extends AbstractMongoAdapter impleme while (it.hasNext()) { UserFederationProviderEntity entity = it.next(); if (entity.getId().equals(provider.getId())) { - session.users().preRemove(this, new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName())); + session.users().preRemove(this, new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName(), + entity.getFullSyncPeriod(), entity.getChangedSyncPeriod(), entity.getLastSync())); it.remove(); } } @@ -799,6 +803,9 @@ public class RealmAdapter extends AbstractMongoAdapter impleme if (displayName != null) { entity.setDisplayName(model.getDisplayName()); } + entity.setFullSyncPeriod(model.getFullSyncPeriod()); + entity.setChangedSyncPeriod(model.getChangedSyncPeriod()); + entity.setLastSync(model.getLastSync()); } } updateRealm(); @@ -822,7 +829,8 @@ public class RealmAdapter extends AbstractMongoAdapter impleme }); List result = new LinkedList(); for (UserFederationProviderEntity entity : copy) { - result.add(new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName())); + result.add(new UserFederationProviderModel(entity.getId(), entity.getProviderName(), entity.getConfig(), entity.getPriority(), entity.getDisplayName(), + entity.getFullSyncPeriod(), entity.getChangedSyncPeriod(), entity.getLastSync())); } return result; @@ -843,6 +851,9 @@ public class RealmAdapter extends AbstractMongoAdapter impleme entity.setDisplayName(entity.getId()); } entity.setDisplayName(displayName); + entity.setFullSyncPeriod(model.getFullSyncPeriod()); + entity.setChangedSyncPeriod(model.getChangedSyncPeriod()); + entity.setLastSync(model.getLastSync()); entities.add(entity); } 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 ec1ba5866a..6fc5237335 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 Marek Posolda @@ -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/services/src/main/java/org/keycloak/services/managers/PeriodicSyncManager.java b/services/src/main/java/org/keycloak/services/managers/PeriodicSyncManager.java new file mode 100644 index 0000000000..7a86529899 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/PeriodicSyncManager.java @@ -0,0 +1,105 @@ +package org.keycloak.services.managers; + +import java.util.List; + +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.utils.KeycloakModelUtils; +import org.keycloak.timer.TimerProvider; +import org.keycloak.util.Time; + +/** + * @author Marek Posolda + */ +public class PeriodicSyncManager { + + /** + * Check federationProviderModel of all realms and possibly start periodic sync for them + * + * @param sessionFactory + * @param timer + */ + public void bootstrap(final KeycloakSessionFactory sessionFactory, final TimerProvider timer) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + List realms = session.realms().getRealms(); + for (final RealmModel realm : realms) { + List federationProviders = realm.getUserFederationProviders(); + for (final UserFederationProviderModel fedProvider : federationProviders) { + startPeriodicSyncForProvider(sessionFactory, timer, fedProvider, realm.getId()); + } + } + } + }); + } + + public void startPeriodicSyncForProvider(final KeycloakSessionFactory sessionFactory, TimerProvider timer, final UserFederationProviderModel fedProvider, final String realmId) { + final UserFederationProviderFactory fedProviderFactory = (UserFederationProviderFactory) sessionFactory.getProviderFactory(UserFederationProvider.class, fedProvider.getProviderName()); + + 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); + } + + }, fedProvider.getFullSyncPeriod() * 1000, fedProvider.getId() + "-FULL"); + } + + if (fedProvider.getChangedSyncPeriod() > 0) { + // We want periodic sync of just changed users for this provider + timer.schedule(new Runnable() { + + @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)); + } + + }, fedProvider.getChangedSyncPeriod() * 1000, fedProvider.getId() + "-CHANGED"); + + } + } + + public void removePeriodicSyncForProvider(TimerProvider timer, final UserFederationProviderModel fedProvider) { + timer.cancelTask(fedProvider.getId() + "-FULL"); + timer.cancelTask(fedProvider.getId() + "-CHANGED"); + } + + // Update interval of last sync for given UserFederationProviderModel. Do it in separate transaction + private void updateLastSyncInterval(final KeycloakSessionFactory sessionFactory, final UserFederationProviderModel fedProvider, final String realmId) { + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + + @Override + public void run(KeycloakSession session) { + RealmModel persistentRealm = session.realms().getRealm(realmId); + List persistentFedProviders = persistentRealm.getUserFederationProviders(); + for (UserFederationProviderModel persistentFedProvider : persistentFedProviders) { + if (fedProvider.getId().equals(persistentFedProvider.getId())) { + // Update persistent provider in DB + int lastSync = Time.currentTime(); + persistentFedProvider.setLastSync(lastSync); + persistentRealm.updateUserFederationProvider(persistentFedProvider); + + // Update "cached" reference + fedProvider.setLastSync(lastSync); + } + } + } + + }); + } + +} 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 ae1e116aa8..c8b613cb93 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -12,12 +12,14 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; +import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.RealmAuditRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.timer.TimerProvider; import java.util.Collections; import java.util.HashSet; @@ -127,6 +129,8 @@ public class RealmManager { } public boolean removeRealm(RealmModel realm) { + List federationProviders = realm.getUserFederationProviders(); + boolean removed = model.removeRealm(realm.getId()); if (removed) { new ApplicationManager(this).removeApplication(getKeycloakAdminstrationRealm(), realm.getMasterAdminApp()); @@ -135,6 +139,12 @@ public class RealmManager { if (sessions != null) { sessions.onRealmRemoved(realm); } + + // Remove all periodic syncs for configured federation providers + PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager(); + for (final UserFederationProviderModel fedProvider : federationProviders) { + periodicSyncManager.removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), fedProvider); + } } return removed; } @@ -202,6 +212,13 @@ public class RealmManager { public void importRealm(RealmRepresentation rep, RealmModel newRealm) { RepresentationToModel.importRealm(session, rep, newRealm); + + // Refresh periodic sync tasks for configured federationProviders + List federationProviders = newRealm.getUserFederationProviders(); + PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager(); + for (final UserFederationProviderModel fedProvider : federationProviders) { + periodicSyncManager.startPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, newRealm.getId()); + } } /** 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 77da618070..ca8db44e4c 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -84,8 +84,8 @@ public class KeycloakApplication extends Application { setupDefaultRealm(context.getContextPath()); - setupScheduledTasks(sessionFactory); importRealms(context); + setupScheduledTasks(sessionFactory); } public String getContextPath() { @@ -146,8 +146,8 @@ public class KeycloakApplication extends Application { long interval = Config.scope("scheduled").getLong("interval", 60L) * 1000; TimerProvider timer = sessionFactory.create().getProvider(TimerProvider.class); - timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredAuditEvents()), interval); - timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval); + timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredAuditEvents()), interval, "ClearExpiredAuditEvents"); + timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions"); } 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 9344d28a87..b6af7a46e9 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 @@ -12,6 +12,7 @@ import org.keycloak.models.ApplicationModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.CacheUserProvider; @@ -21,10 +22,12 @@ 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.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.resources.flows.Flows; +import org.keycloak.timer.TimerProvider; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -160,6 +163,13 @@ public class RealmAdminResource { cache.setEnabled(rep.isUserCacheEnabled()); } + // Refresh periodic sync tasks for configured federationProviders + List federationProviders = realm.getUserFederationProviders(); + PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager(); + for (final UserFederationProviderModel fedProvider : federationProviders) { + periodicSyncManager.startPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId()); + } + return Response.noContent().build(); } catch (ModelDuplicateException e) { return Flows.errors().exists("Realm " + rep.getRealm() + " already exists"); 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 c156c4b789..6039ccbade 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,6 +12,8 @@ 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.timer.TimerProvider; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -116,7 +118,10 @@ public class UserFederationResource { if (displayName != null && displayName.trim().equals("")) { displayName = null; } - UserFederationProviderModel model = realm.addUserFederationProvider(rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName); + 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()); + return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); } @@ -136,8 +141,10 @@ public class UserFederationResource { if (displayName != null && displayName.trim().equals("")) { displayName = null; } - UserFederationProviderModel model = new UserFederationProviderModel(id, rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName); + 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()); } /** @@ -170,9 +177,9 @@ public class UserFederationResource { public void deleteProviderInstance(@PathParam("id") String id) { logger.info("deleteProvider"); auth.requireManage(); - UserFederationProviderModel model = new UserFederationProviderModel(id, null, null, -1, null); + UserFederationProviderModel model = new UserFederationProviderModel(id, null, null, -1, null, -1, -1, 0); realm.removeUserFederationProvider(model); - + new PeriodicSyncManager().removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), model); } 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 04ac0a17ba..753409a1eb 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 a5a0b6eeda..2809544673 100755 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProviderFactory.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProviderFactory.java @@ -1,19 +1,29 @@ package org.keycloak.testutils; +import org.jboss.logging.Logger; 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; +import java.util.concurrent.atomic.AtomicInteger; /** * @author Bill Burke * @version $Revision: 1 $ */ public class DummyUserFederationProviderFactory implements UserFederationProviderFactory { + + private static final Logger logger = Logger.getLogger(DummyUserFederationProviderFactory.class); + public static final String PROVIDER_NAME = "dummy"; + + private AtomicInteger fullSyncCounter = new AtomicInteger(); + private AtomicInteger changedSyncCounter = new AtomicInteger(); + @Override public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) { return new DummyUserFederationProvider(); @@ -43,6 +53,26 @@ public class DummyUserFederationProviderFactory implements UserFederationProvide @Override public String getId() { - return "dummy"; + return PROVIDER_NAME; + } + + @Override + public void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) { + logger.info("syncAllUsers invoked"); + fullSyncCounter.incrementAndGet(); + } + + @Override + public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) { + logger.info("syncChangedUsers invoked"); + changedSyncCounter.incrementAndGet(); + } + + public int getFullSyncCounter() { + return fullSyncCounter.get(); + } + + public int getChangedSyncCounter() { + return changedSyncCounter.get(); } } 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 466952a570..1c10b2429c 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 6119852bcc..c275c2fb15 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 791e686f98..99f6558117 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 @@ -60,7 +60,7 @@ public class FederationProvidersIntegrationTest { ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "true"); ldapConfig.put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString()); - ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap"); + ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0); // Delete all LDAP users and add some new for testing PartitionManager partitionManager = getPartitionManager(manager.getSession(), ldapModel); @@ -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); @@ -166,7 +166,7 @@ public class FederationProvidersIntegrationTest { RealmManager manager = new RealmManager(session); RealmModel appRealm = manager.getRealm("test"); - ldapModel = appRealm.addUserFederationProvider(ldapModel.getProviderName(), ldapModel.getConfig(), ldapModel.getPriority(), ldapModel.getDisplayName()); + ldapModel = appRealm.addUserFederationProvider(ldapModel.getProviderName(), ldapModel.getConfig(), ldapModel.getPriority(), ldapModel.getDisplayName(), -1, -1, 0); } finally { keycloakRule.stopSession(session, true); } @@ -233,7 +233,8 @@ public class FederationProvidersIntegrationTest { try { RealmModel appRealm = session.realms().getRealmByName("test"); - UserFederationProviderModel model = new UserFederationProviderModel(ldapModel.getId(), ldapModel.getProviderName(), ldapModel.getConfig(), ldapModel.getPriority(), ldapModel.getDisplayName()); + UserFederationProviderModel model = new UserFederationProviderModel(ldapModel.getId(), ldapModel.getProviderName(), ldapModel.getConfig(), + ldapModel.getPriority(), ldapModel.getDisplayName(), -1, -1, 0); model.getConfig().put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString()); appRealm.updateUserFederationProvider(model); UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm); @@ -284,7 +285,8 @@ public class FederationProvidersIntegrationTest { try { RealmModel appRealm = session.realms().getRealmByName("test"); - UserFederationProviderModel model = new UserFederationProviderModel(ldapModel.getId(), ldapModel.getProviderName(), ldapModel.getConfig(), ldapModel.getPriority(), ldapModel.getDisplayName()); + UserFederationProviderModel model = new UserFederationProviderModel(ldapModel.getId(), ldapModel.getProviderName(), ldapModel.getConfig(), ldapModel.getPriority(), + ldapModel.getDisplayName(), -1, -1, 0); model.getConfig().put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.UNSYNCED.toString()); appRealm.updateUserFederationProvider(model); UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm); @@ -313,7 +315,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 0000000000..21ac2901fa --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SyncProvidersTest.java @@ -0,0 +1,186 @@ +package org.keycloak.testsuite.forms; + +import java.util.Date; +import java.util.HashMap; +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.examples.federation.properties.ClasspathPropertiesFederationFactory; +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.PeriodicSyncManager; +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; +import org.picketlink.idm.PartitionManager; +import org.picketlink.idm.model.basic.User; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class SyncProvidersTest { + + private static LDAPRule ldapRule = new LDAPRule(); + + private static UserFederationProviderModel ldapModel = null; + private static UserFederationProviderModel dummyModel = 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 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", + -1, -1, 0); + + // 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 + dummyModel = appRealm.addUserFederationProvider(DummyUserFederationProviderFactory.PROVIDER_NAME, new HashMap(), 1, "test-dummy", -1, 1, 0); + } + }); + + @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 + sleep(1000); + 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); + } + } + + @Test + public void testPeriodicSync() { + KeycloakSession session = keycloakRule.startSession(); + try { + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + DummyUserFederationProviderFactory dummyFedFactory = (DummyUserFederationProviderFactory)sessionFactory.getProviderFactory(UserFederationProvider.class, DummyUserFederationProviderFactory.PROVIDER_NAME); + int full = dummyFedFactory.getFullSyncCounter(); + int changed = dummyFedFactory.getChangedSyncCounter(); + + // Assert that after some period was DummyUserFederationProvider triggered + PeriodicSyncManager periodicSyncManager = new PeriodicSyncManager(); + periodicSyncManager.bootstrap(sessionFactory, session.getProvider(TimerProvider.class)); + sleep(1800); + + // Cancel timer + periodicSyncManager.removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), dummyModel); + + // Assert that DummyUserFederationProviderFactory.syncChangedUsers was invoked + int newChanged = dummyFedFactory.getChangedSyncCounter(); + Assert.assertEquals(full, dummyFedFactory.getFullSyncCounter()); + Assert.assertTrue(newChanged > changed); + + // Assert that dummy provider won't be invoked anymore + sleep(1800); + Assert.assertEquals(full, dummyFedFactory.getFullSyncCounter()); + Assert.assertEquals(newChanged, dummyFedFactory.getChangedSyncCounter()); + } finally { + keycloakRule.stopSession(session, false); + } + } + + private void sleep(int time) { + try { + Thread.sleep(time); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + } + + 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()); + } +} diff --git a/timer/api/src/main/java/org/keycloak/timer/TimerProvider.java b/timer/api/src/main/java/org/keycloak/timer/TimerProvider.java index 627f00e3f5..a8396de6db 100644 --- a/timer/api/src/main/java/org/keycloak/timer/TimerProvider.java +++ b/timer/api/src/main/java/org/keycloak/timer/TimerProvider.java @@ -7,6 +7,8 @@ import org.keycloak.provider.Provider; */ public interface TimerProvider extends Provider { - public void schedule(Runnable runnable, long interval); + public void schedule(Runnable runnable, long interval, String taskName); + + public void cancelTask(String taskName); } diff --git a/timer/basic/pom.xml b/timer/basic/pom.xml index 268c76a4a6..658e4c50c9 100755 --- a/timer/basic/pom.xml +++ b/timer/basic/pom.xml @@ -31,6 +31,11 @@ ${project.version} provided + + org.jboss.logging + jboss-logging + provided + diff --git a/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java index 267f17cae1..aa1a3f9e66 100644 --- a/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java +++ b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProvider.java @@ -1,5 +1,6 @@ package org.keycloak.timer.basic; +import org.jboss.logging.Logger; import org.keycloak.timer.TimerProvider; import java.util.Timer; @@ -10,15 +11,18 @@ import java.util.TimerTask; */ public class BasicTimerProvider implements TimerProvider { - private Timer timer; + private static final Logger logger = Logger.getLogger(BasicTimerProvider.class); - public BasicTimerProvider(Timer timer) { + private final Timer timer; + private final BasicTimerProviderFactory factory; + public BasicTimerProvider(Timer timer, BasicTimerProviderFactory factory) { this.timer = timer; + this.factory = factory; } @Override - public void schedule(final Runnable runnable, final long interval) { + public void schedule(final Runnable runnable, final long interval, String taskName) { TimerTask task = new TimerTask() { @Override public void run() { @@ -26,9 +30,25 @@ public class BasicTimerProvider implements TimerProvider { } }; + TimerTask existingTask = factory.putTask(taskName, task); + if (existingTask != null) { + logger.infof("Existing timer task '%s' found. Cancelling it", taskName); + existingTask.cancel(); + } + + logger.infof("Starting task '%s' with interval '%d'", taskName, interval); timer.schedule(task, interval, interval); } + @Override + public void cancelTask(String taskName) { + TimerTask existingTask = factory.removeTask(taskName); + if (existingTask != null) { + logger.infof("Cancelling task '%s'", taskName); + existingTask.cancel(); + } + } + @Override public void close() { // do nothing diff --git a/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java index b0e5063e96..0069b2cfcd 100644 --- a/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java +++ b/timer/basic/src/main/java/org/keycloak/timer/basic/BasicTimerProviderFactory.java @@ -6,6 +6,9 @@ import org.keycloak.timer.TimerProvider; import org.keycloak.timer.TimerProviderFactory; import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * @author Stian Thorgersen @@ -14,9 +17,11 @@ public class BasicTimerProviderFactory implements TimerProviderFactory { private Timer timer; + private ConcurrentMap scheduledTasks = new ConcurrentHashMap(); + @Override public TimerProvider create(KeycloakSession session) { - return new BasicTimerProvider(timer); + return new BasicTimerProvider(timer, this); } @Override @@ -35,4 +40,12 @@ public class BasicTimerProviderFactory implements TimerProviderFactory { return "basic"; } + protected TimerTask putTask(String taskName, TimerTask task) { + return scheduledTasks.put(taskName, task); + } + + protected TimerTask removeTask(String taskName) { + return scheduledTasks.remove(taskName); + } + }