Sync - first iteration

This commit is contained in:
mposolda 2014-08-08 21:01:34 +02:00
parent 5cb6c4e77c
commit ee79747cb6
25 changed files with 486 additions and 107 deletions

View file

@ -2,12 +2,19 @@ package org.keycloak.examples.federation.properties;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
@ -84,4 +91,44 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
public void close() {
}
@Override
public void syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
RealmModel realm = session.realms().getRealm(realmId);
BasePropertiesFederationProvider federationProvider = (BasePropertiesFederationProvider)getInstance(session, model);
Set<String> allUsernames = federationProvider.getProperties().stringPropertyNames();
for (String username : allUsernames) {
federationProvider.getUserByUsername(realm, username);
}
}
});
}
@Override
public void syncChangedUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model, Date lastSync) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
RealmModel realm = session.realms().getRealm(realmId);
UserProvider localProvider = session.userStorage();
BasePropertiesFederationProvider federationProvider = (BasePropertiesFederationProvider)getInstance(session, model);
Set<String> allUsernames = federationProvider.getProperties().stringPropertyNames();
for (String username : allUsernames) {
UserModel localUser = localProvider.getUserByUsername(username, realm);
if (localUser == null) {
// New user, let's import him
federationProvider.getUserByUsername(realm, username);
}
}
}
});
}
}

View file

@ -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;
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ExportImportJob {
public void run(KeycloakSession session) throws IOException;
}

View file

@ -0,0 +1,25 @@
package org.keycloak.exportimport.util;
import java.io.IOException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionTask;
/**
* Just to wrap {@link IOException}
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class ExportImportSessionTask implements KeycloakSessionTask {
@Override
public void run(KeycloakSession session) {
try {
runExportImportTask(session);
} catch (IOException ioe) {
throw new RuntimeException("Error during export/import: " + ioe.getMessage(), ioe);
}
}
protected abstract void runExportImportTask(KeycloakSession session) throws IOException;
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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";
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
});

View file

@ -3,12 +3,13 @@ package org.keycloak.exportimport.singlefile;
import org.codehaus.jackson.map.ObjectMapper;
import org.jboss.logging.Logger;
import org.keycloak.exportimport.ExportProvider;
import org.keycloak.exportimport.util.ExportImportJob;
import org.keycloak.exportimport.util.ExportImportUtils;
import org.keycloak.exportimport.util.ExportImportSessionTask;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.util.JsonSerialization;
@ -38,10 +39,10 @@ public class SingleFileExportProvider implements ExportProvider {
@Override
public void exportModel(KeycloakSessionFactory factory) throws IOException {
logger.infof("Exporting model into file %s", this.file.getAbsolutePath());
ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
public void run(KeycloakSession session) throws IOException {
protected void runExportImportTask(KeycloakSession session) throws IOException {
List<RealmModel> realms = session.realms().getRealms();
List<RealmRepresentation> reps = new ArrayList<RealmRepresentation>();
for (RealmModel realm : realms) {
@ -58,10 +59,10 @@ public class SingleFileExportProvider implements ExportProvider {
@Override
public void exportRealm(KeycloakSessionFactory factory, final String realmName) throws IOException {
logger.infof("Exporting realm '%s' into file %s", realmName, this.file.getAbsolutePath());
ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
public void run(KeycloakSession session) throws IOException {
protected void runExportImportTask(KeycloakSession session) throws IOException {
RealmModel realm = session.realms().getRealmByName(realmName);
RealmRepresentation realmRep = ExportUtils.exportRealm(session, realm, true);
writeToFile(realmRep);

View file

@ -3,16 +3,15 @@ package org.keycloak.exportimport.singlefile;
import org.jboss.logging.Logger;
import org.keycloak.exportimport.ImportProvider;
import org.keycloak.exportimport.Strategy;
import org.keycloak.exportimport.util.ExportImportJob;
import org.keycloak.exportimport.util.ExportImportUtils;
import org.keycloak.exportimport.util.ImportUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.util.JsonSerialization;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import org.keycloak.exportimport.util.ExportImportSessionTask;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -30,10 +29,10 @@ public class SingleFileImportProvider implements ImportProvider {
@Override
public void importModel(KeycloakSessionFactory factory, final Strategy strategy) throws IOException {
logger.infof("Full importing from file %s", this.file.getAbsolutePath());
ExportImportUtils.runJobInTransaction(factory, new ExportImportJob() {
KeycloakModelUtils.runJobInTransaction(factory, new ExportImportSessionTask() {
@Override
public void run(KeycloakSession session) throws IOException {
protected void runExportImportTask(KeycloakSession session) throws IOException {
FileInputStream is = new FileInputStream(file);
ImportUtils.importFromStream(session, JsonSerialization.mapper, is, strategy);
}

View file

@ -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);
}
});

View file

@ -292,4 +292,29 @@ public class LDAPFederationProvider implements UserFederationProvider {
public void close() {
//To change body of implemented methods use File | Settings | File Templates.
}
protected void importPicketlinkUsers(RealmModel realm, List<User> users, UserFederationProviderModel fedModel) {
for (User picketlinkUser : users) {
String username = picketlinkUser.getLoginName();
UserModel currentUser = session.userStorage().getUserByUsername(username, realm);
if (currentUser == null) {
// Add new user to Keycloak
importUserFromPicketlink(realm, picketlinkUser);
logger.infof("Added new user from LDAP: " + username);
} else {
if ((fedModel.getId().equals(currentUser.getFederationLink())) && (picketlinkUser.getId().equals(currentUser.getAttribute(LDAPFederationProvider.LDAP_ID)))) {
// Update keycloak user
String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null;
currentUser.setEmail(email);
currentUser.setFirstName(picketlinkUser.getFirstName());
currentUser.setLastName(picketlinkUser.getLastName());
logger.infof("Updated user from LDAP: " + currentUser.getUsername());
} else {
// TODO: We have local user of same username like LDAP user, but not linked. What to do? Delete him and import again?
throw new IllegalStateException("User " + username + " has invalid LDAP ID or doesn't have federation link");
}
}
}
}
}

View file

@ -1,14 +1,26 @@
package org.keycloak.federation.ldap;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.picketlink.PartitionManagerProvider;
import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.model.IdentityType;
import org.picketlink.idm.model.basic.User;
import org.picketlink.idm.query.IdentityQuery;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
/**
@ -17,6 +29,7 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class LDAPFederationProviderFactory implements UserFederationProviderFactory {
private static final Logger logger = Logger.getLogger(LDAPFederationProviderFactory.class);
public static final String PROVIDER_NAME = "ldap";
@Override
@ -25,7 +38,7 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
}
@Override
public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
public LDAPFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
PartitionManagerProvider idmProvider = session.getProvider(PartitionManagerProvider.class);
PartitionManager partition = idmProvider.getPartitionManager(model);
return new LDAPFederationProvider(session, model, partition);
@ -49,4 +62,76 @@ public class LDAPFederationProviderFactory implements UserFederationProviderFact
public Set<String> getConfigurationOptions() {
return Collections.emptySet();
}
@Override
public void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) {
logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s, current time: " + new Date(), realmId, model.getDisplayName());
PartitionManagerProvider idmProvider = sessionFactory.create().getProvider(PartitionManagerProvider.class);
PartitionManager partitionMgr = idmProvider.getPartitionManager(model);
IdentityQuery<User> userQuery = partitionMgr.createIdentityManager().createIdentityQuery(User.class);
syncImpl(sessionFactory, userQuery, realmId, model);
// TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
}
@Override
public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, current time: " + new Date() + ", last sync time: " + lastSync, realmId, model.getDisplayName());
PartitionManagerProvider idmProvider = sessionFactory.create().getProvider(PartitionManagerProvider.class);
PartitionManager partitionMgr = idmProvider.getPartitionManager(model);
// Sync newly created users
IdentityManager identityManager = partitionMgr.createIdentityManager();
IdentityQuery<User> userQuery = identityManager.createIdentityQuery(User.class)
.setParameter(IdentityType.CREATED_AFTER, lastSync);
syncImpl(sessionFactory, userQuery, realmId, model);
// Sync updated users
userQuery = identityManager.createIdentityQuery(User.class)
.setParameter(IdentityType.MODIFIED_AFTER, lastSync);
syncImpl(sessionFactory, userQuery, realmId, model);
}
protected void syncImpl(KeycloakSessionFactory sessionFactory, IdentityQuery<User> userQuery, final String realmId, final UserFederationProviderModel fedModel) {
boolean pagination = Boolean.parseBoolean(fedModel.getConfig().get(LDAPConstants.PAGINATION));
if (pagination) {
String pageSizeConfig = fedModel.getConfig().get(LDAPConstants.BATCH_SIZE_FOR_SYNC);
int pageSize = pageSizeConfig!=null ? Integer.parseInt(pageSizeConfig) : LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC;
boolean nextPage = true;
while (nextPage) {
userQuery.setLimit(pageSize);
final List<User> users = userQuery.getResultList();
nextPage = userQuery.getPaginationContext() != null;
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
importPicketlinkUsers(session, realmId, fedModel, users);
}
});
}
} else {
// LDAP pagination not available. Do everything in single transaction
final List<User> users = userQuery.getResultList();
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
importPicketlinkUsers(session, realmId, fedModel, users);
}
});
}
}
protected void importPicketlinkUsers(KeycloakSession session, String realmId, UserFederationProviderModel fedModel, List<User> users) {
RealmModel realm = session.realms().getRealm(realmId);
LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel);
ldapFedProvider.importPicketlinkUsers(realm, users, fedModel);
}
}

View file

@ -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);

View file

@ -0,0 +1,12 @@
package org.keycloak.models;
/**
* Task to be executed inside transaction
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface KeycloakSessionTask {
public void run(KeycloakSession session);
}

View file

@ -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";
}

View file

@ -2,6 +2,7 @@ package org.keycloak.models;
import org.keycloak.provider.ProviderFactory;
import java.util.Date;
import java.util.Set;
/**
@ -32,4 +33,26 @@ public interface UserFederationProviderFactory extends ProviderFactory<UserFeder
*/
@Override
String getId();
/**
* Sync all users from the provider storage to Keycloak storage.
*
* @param sessionFactory
* @param realmId
* @param model
*/
void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model);
/**
* Sync just changed (added / updated / removed) users from the provider storage to Keycloak storage. This is useful in case
* that your storage supports "changelogs" (Tracking what users changed since specified date). It's implementation specific to
* decide what exactly will be changed (For example LDAP supports tracking of added / updated users, but not removed users. So
* removed users are not synced)
*
* @param sessionFactory
* @param realmId
* @param model
* @param lastSync
*/
void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync);
}

View file

@ -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";
}
}

View file

@ -11,13 +11,17 @@ import org.picketlink.idm.config.LDAPMappingConfigurationBuilder;
import org.picketlink.idm.config.LDAPStoreConfigurationBuilder;
import org.picketlink.idm.internal.DefaultPartitionManager;
import org.picketlink.idm.model.basic.User;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import static org.picketlink.common.constants.LDAPConstants.*;
import static org.picketlink.common.constants.LDAPConstants.CN;
import static org.picketlink.common.constants.LDAPConstants.EMAIL;
import static org.picketlink.common.constants.LDAPConstants.SN;
import static org.picketlink.common.constants.LDAPConstants.UID;
import static org.picketlink.common.constants.LDAPConstants.CREATE_TIMESTAMP;
import static org.picketlink.common.constants.LDAPConstants.MODIFY_TIMESTAMP;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -80,6 +84,8 @@ public class PartitionManagerRegistry {
}
String ldapFirstNameMapping = activeDirectory ? "givenName" : CN;
String createTimestampMapping = activeDirectory ? "whenCreated" : CREATE_TIMESTAMP;
String modifyTimestampMapping = activeDirectory ? "whenChanged" : MODIFY_TIMESTAMP;
String[] userObjectClasses = getUserObjectClasses(ldapConfig);
boolean pagination = ldapConfig.containsKey(LDAPConstants.PAGINATION) ? Boolean.parseBoolean(ldapConfig.get(LDAPConstants.PAGINATION)) : false;
@ -112,7 +118,9 @@ public class PartitionManagerRegistry {
.attribute("loginName", ldapLoginNameMapping, true)
.attribute("firstName", ldapFirstNameMapping)
.attribute("lastName", SN)
.attribute("email", EMAIL);
.attribute("email", EMAIL)
.readOnlyAttribute("createdDate", createTimestampMapping)
.readOnlyAttribute("modifyDate", modifyTimestampMapping);
if (activeDirectory && ldapLoginNameMapping.equals("sAMAccountName")) {
ldapUserMappingBuilder.bindingAttribute("fullName", CN);

View file

@ -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

View file

@ -2,10 +2,11 @@ package org.keycloak.testutils;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@ -45,4 +46,12 @@ public class DummyUserFederationProviderFactory implements UserFederationProvide
public String getId() {
return "dummy";
}
@Override
public void syncAllUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model) {
}
@Override
public void syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
}
}

View file

@ -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

View file

@ -9,3 +9,4 @@ 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
idm.test.ldap.batch.size.for.sync=3

View file

@ -102,7 +102,7 @@ public class FederationProvidersIntegrationTest {
@WebResource
protected AccountPasswordPage changePasswordPage;
private static UserModel addUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
static UserModel addUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
UserModel user = session.users().addUser(realm, username);
user.setEmail(email);
user.setEnabled(true);
@ -313,7 +313,7 @@ public class FederationProvidersIntegrationTest {
}
}
private static PartitionManager getPartitionManager(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
static PartitionManager getPartitionManager(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
PartitionManagerProvider partitionManagerProvider = keycloakSession.getProvider(PartitionManagerProvider.class);
return partitionManagerProvider.getPartitionManager(ldapFedModel);
}

View file

@ -0,0 +1,145 @@
package org.keycloak.testsuite.forms;
import java.util.Date;
import java.util.Map;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runners.MethodSorters;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.federation.ldap.LDAPUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.LDAPRule;
import org.keycloak.testutils.LDAPEmbeddedServer;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.model.basic.User;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SyncProvidersTest {
private static LDAPRule ldapRule = new LDAPRule();
private static UserFederationProviderModel ldapModel = null;
private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
LDAPEmbeddedServer ldapServer = ldapRule.getEmbeddedServer();
Map<String,String> ldapConfig = ldapServer.getLDAPConfig();
ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "false");
ldapConfig.put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.UNSYNCED.toString());
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap");
// Delete all LDAP users and add 5 new users for testing
PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(manager.getSession(), ldapModel);
LDAPUtils.removeAllUsers(partitionManager);
User user1 = LDAPUtils.addUser(partitionManager, "user1", "User1FN", "User1LN", "user1@email.org");
LDAPUtils.updatePassword(partitionManager, user1, "password1");
User user2 = LDAPUtils.addUser(partitionManager, "user2", "User2FN", "User2LN", "user2@email.org");
LDAPUtils.updatePassword(partitionManager, user2, "password2");
User user3 = LDAPUtils.addUser(partitionManager, "user3", "User3FN", "User3LN", "user3@email.org");
LDAPUtils.updatePassword(partitionManager, user3, "password3");
User user4 = LDAPUtils.addUser(partitionManager, "user4", "User4FN", "User4LN", "user4@email.org");
LDAPUtils.updatePassword(partitionManager, user4, "password4");
User user5 = LDAPUtils.addUser(partitionManager, "user5", "User5FN", "User5LN", "user5@email.org");
LDAPUtils.updatePassword(partitionManager, user5, "password5");
// Add properties provider
// Map<String,String> filePropertiesConfig = new HashMap<String, String>();
// filePropertiesConfig.put("path", );
// appRealm.addUserFederationProvider(FilePropertiesFederationFactory.PROVIDER_NAME, filePropertiesConfig, 1, "test-fileProps");
}
});
@ClassRule
public static TestRule chain = RuleChain
.outerRule(ldapRule)
.around(keycloakRule);
@Test
public void testLDAPSync() {
KeycloakSession session = keycloakRule.startSession();
try {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
UserFederationProviderFactory ldapFedFactory = (UserFederationProviderFactory)sessionFactory.getProviderFactory(UserFederationProvider.class, LDAPFederationProviderFactory.PROVIDER_NAME);
ldapFedFactory.syncAllUsers(sessionFactory, "test", ldapModel);
} finally {
keycloakRule.stopSession(session, false);
}
// Assert users imported (model test)
session = keycloakRule.startSession();
try {
RealmModel testRealm = session.realms().getRealm("test");
UserProvider userProvider = session.userStorage();
assertUserImported(userProvider, testRealm, "user1", "User1FN", "User1LN", "user1@email.org");
assertUserImported(userProvider, testRealm, "user2", "User2FN", "User2LN", "user2@email.org");
assertUserImported(userProvider, testRealm, "user3", "User3FN", "User3LN", "user3@email.org");
assertUserImported(userProvider, testRealm, "user4", "User4FN", "User4LN", "user4@email.org");
assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org");
// wait a bit
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
Date beforeLDAPUpdate = new Date();
// Add user to LDAP and update 'user5' in LDAP
PartitionManager partitionManager = FederationProvidersIntegrationTest.getPartitionManager(session, ldapModel);
LDAPUtils.addUser(partitionManager, "user6", "User6FN", "User6LN", "user6@email.org");
LDAPUtils.updateUser(partitionManager, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
// Assert still old users in local provider
assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org");
Assert.assertNull(userProvider.getUserByUsername("user6", testRealm));
// Trigger partial sync
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
UserFederationProviderFactory ldapFedFactory = (UserFederationProviderFactory)sessionFactory.getProviderFactory(UserFederationProvider.class, LDAPFederationProviderFactory.PROVIDER_NAME);
ldapFedFactory.syncChangedUsers(sessionFactory, "test", ldapModel, beforeLDAPUpdate);
} finally {
keycloakRule.stopSession(session, false);
}
session = keycloakRule.startSession();
try {
RealmModel testRealm = session.realms().getRealm("test");
UserProvider userProvider = session.userStorage();
// Assert users updated in local provider
assertUserImported(userProvider, testRealm, "user5", "User5FNUpdated", "User5LNUpdated", "user5Updated@email.org");
assertUserImported(userProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org");
} finally {
keycloakRule.stopSession(session, false);
}
}
private void assertUserImported(UserProvider userProvider, RealmModel realm, String username, String expectedFirstName, String expectedLastName, String expectedEmail) {
UserModel user = userProvider.getUserByUsername(username, realm);
Assert.assertNotNull(user);
Assert.assertEquals(expectedFirstName, user.getFirstName());
Assert.assertEquals(expectedLastName, user.getLastName());
Assert.assertEquals(expectedEmail, user.getEmail());
}
}