Added sync support to UserFederationMapper

This commit is contained in:
mposolda 2015-12-16 12:25:21 +01:00
parent 358c273d39
commit 0d52e4e6c5
19 changed files with 294 additions and 128 deletions

View file

@ -0,0 +1,56 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFederationMapperSyncConfigRepresentation {
private Boolean fedToKeycloakSyncSupported;
private String fedToKeycloakSyncMessage; // applicable just if fedToKeycloakSyncSupported is true
private Boolean keycloakToFedSyncSupported;
private String keycloakToFedSyncMessage; // applicable just if keycloakToFedSyncSupported is true
public UserFederationMapperSyncConfigRepresentation() {
}
public UserFederationMapperSyncConfigRepresentation(boolean fedToKeycloakSyncSupported, String fedToKeycloakSyncMessage,
boolean keycloakToFedSyncSupported, String keycloakToFedSyncMessage) {
this.fedToKeycloakSyncSupported = fedToKeycloakSyncSupported;
this.fedToKeycloakSyncMessage = fedToKeycloakSyncMessage;
this.keycloakToFedSyncSupported = keycloakToFedSyncSupported;
this.keycloakToFedSyncMessage = keycloakToFedSyncMessage;
}
public Boolean isFedToKeycloakSyncSupported() {
return fedToKeycloakSyncSupported;
}
public void setFedToKeycloakSyncSupported(Boolean fedToKeycloakSyncSupported) {
this.fedToKeycloakSyncSupported = fedToKeycloakSyncSupported;
}
public String getFedToKeycloakSyncMessage() {
return fedToKeycloakSyncMessage;
}
public void setFedToKeycloakSyncMessage(String fedToKeycloakSyncMessage) {
this.fedToKeycloakSyncMessage = fedToKeycloakSyncMessage;
}
public Boolean isKeycloakToFedSyncSupported() {
return keycloakToFedSyncSupported;
}
public void setKeycloakToFedSyncSupported(Boolean keycloakToFedSyncSupported) {
this.keycloakToFedSyncSupported = keycloakToFedSyncSupported;
}
public String getKeycloakToFedSyncMessage() {
return keycloakToFedSyncMessage;
}
public void setKeycloakToFedSyncMessage(String keycloakToFedSyncMessage) {
this.keycloakToFedSyncMessage = keycloakToFedSyncMessage;
}
}

View file

@ -12,6 +12,8 @@ public class UserFederationMapperTypeRepresentation {
protected String category; protected String category;
protected String helpText; protected String helpText;
protected UserFederationMapperSyncConfigRepresentation syncConfig;
protected List<ConfigPropertyRepresentation> properties = new LinkedList<>(); protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
public String getId() { public String getId() {
@ -46,6 +48,14 @@ public class UserFederationMapperTypeRepresentation {
this.helpText = helpText; this.helpText = helpText;
} }
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
return syncConfig;
}
public void setSyncConfig(UserFederationMapperSyncConfigRepresentation syncConfig) {
this.syncConfig = syncConfig;
}
public List<ConfigPropertyRepresentation> getProperties() { public List<ConfigPropertyRepresentation> getProperties() {
return properties; return properties;
} }

View file

@ -1,12 +1,26 @@
package org.keycloak.federation.ldap.mappers; package org.keycloak.federation.ldap.mappers;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationSyncResult;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapper { public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapper {
@Override
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
throw new IllegalStateException("Not supported");
}
@Override
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
throw new IllegalStateException("Not supported");
}
@Override @Override
public void close() { public void close() {

View file

@ -10,6 +10,7 @@ import org.keycloak.mappers.UserFederationMapperFactory;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -35,6 +36,11 @@ public abstract class AbstractLDAPFederationMapperFactory implements UserFederat
public void postInit(KeycloakSessionFactory factory) { public void postInit(KeycloakSessionFactory factory) {
} }
@Override
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
return new UserFederationMapperSyncConfigRepresentation(false, null, false, null);
}
@Override @Override
public void close() { public void close() {
} }

View file

@ -14,12 +14,15 @@ import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery; import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder; import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationSyncResult;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.UserModelDelegate; import org.keycloak.models.utils.UserModelDelegate;
@ -63,15 +66,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
// Customized LDAP filter which is added to the whole LDAP query // Customized LDAP filter which is added to the whole LDAP query
public static final String ROLES_LDAP_FILTER = "roles.ldap.filter"; public static final String ROLES_LDAP_FILTER = "roles.ldap.filter";
// List of IDs of UserFederationMapperModels where syncRolesFromLDAP was already called in this KeycloakSession. This is to improve performance
// TODO: Rather address this with caching at LDAPIdentityStore level?
private Set<String> rolesSyncedModels = new TreeSet<>();
@Override @Override
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) { public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
syncRolesFromLDAP(mapperModel, ldapProvider, realm);
Mode mode = getMode(mapperModel); Mode mode = getMode(mapperModel);
// For now, import LDAP role mappings just during create // For now, import LDAP role mappings just during create
@ -95,17 +91,26 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
@Override @Override
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) { public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
syncRolesFromLDAP(mapperModel, ldapProvider, realm);
} }
// Sync roles from LDAP tree and create them in local Keycloak DB (if they don't exist here yet)
protected void syncRolesFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) { // Sync roles from LDAP to Keycloak DB
if (!rolesSyncedModels.contains(mapperModel.getId())) { @Override
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
@Override
public String getStatus() {
return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated());
}
};
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName()); logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
// Send LDAP query
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
// Send query
List<LDAPObject> ldapRoles = ldapQuery.getResultList(); List<LDAPObject> ldapRoles = ldapQuery.getResultList();
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm); RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
@ -116,13 +121,61 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
if (roleContainer.getRole(roleName) == null) { if (roleContainer.getRole(roleName) == null) {
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName); logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
roleContainer.addRole(roleName); roleContainer.addRole(roleName);
syncResult.increaseAdded();
} else {
syncResult.increaseUpdated();
} }
} }
rolesSyncedModels.add(mapperModel.getId()); return syncResult;
}
// Sync roles from Keycloak back to LDAP
@Override
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
@Override
public String getStatus() {
return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated());
}
};
logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
// Send LDAP query to see which roles exists there
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
Set<String> ldapRoleNames = new HashSet<>();
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
for (LDAPObject ldapRole : ldapRoles) {
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
ldapRoleNames.add(roleName);
}
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
Set<RoleModel> keycloakRoles = roleContainer.getRoles();
for (RoleModel keycloakRole : keycloakRoles) {
String roleName = keycloakRole.getName();
if (ldapRoleNames.contains(roleName)) {
syncResult.increaseUpdated();
} else {
logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName);
createLDAPRole(mapperModel, roleName, ldapProvider);
syncResult.increaseAdded();
} }
} }
return syncResult;
}
public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) { public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider); LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);

View file

@ -5,21 +5,13 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.mappers.MapperConfigValidationException; import org.keycloak.mappers.MapperConfigValidationException;
import org.keycloak.mappers.UserFederationMapper; import org.keycloak.mappers.UserFederationMapper;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderEvent; import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
import org.keycloak.provider.ProviderEventListener;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -58,15 +50,15 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
membershipTypes.add(membershipType.toString()); membershipTypes.add(membershipType.toString());
} }
ProviderConfigProperty membershipType = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type", ProviderConfigProperty membershipType = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type",
"DN means that LDAP role has it's members declared in form of their full DN. For example ( 'member: uid=john,ou=users,dc=example,dc=com' . " + "DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
"UID means that LDAP role has it's members declared in form of pure user uids. For example ( 'memberUid: john' ))", "UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .",
ProviderConfigProperty.LIST_TYPE, membershipTypes); ProviderConfigProperty.LIST_TYPE, membershipTypes);
configProperties.add(membershipType); configProperties.add(membershipType);
ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER, ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER,
"LDAP Filter", "LDAP Filter",
"LDAP Filter adds additional custom filter to the whole query. Make sure that it starts with '(' and ends with ')'", "LDAP Filter adds additional custom filter to the whole query. Leave this empty if no additional filtering is needed. Otherwise make sure that filter starts with '(' and ends with ')'",
ProviderConfigProperty.STRING_TYPE, null); ProviderConfigProperty.STRING_TYPE, null);
configProperties.add(ldapFilter); configProperties.add(ldapFilter);
@ -90,7 +82,7 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
ProviderConfigProperty retriever = createConfigProperty(RoleLDAPFederationMapper.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy", ProviderConfigProperty retriever = createConfigProperty(RoleLDAPFederationMapper.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy",
"Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " + "Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
"GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " + "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
"LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN extension." "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension."
, ,
ProviderConfigProperty.LIST_TYPE, roleRetrievers); ProviderConfigProperty.LIST_TYPE, roleRetrievers);
configProperties.add(retriever); configProperties.add(retriever);
@ -131,40 +123,9 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
return PROVIDER_ID; return PROVIDER_ID;
} }
// Sync roles from LDAP to Keycloak DB during creation or update of mapperModel
@Override @Override
public void postInit(KeycloakSessionFactory factory) { public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
factory.register(new ProviderEventListener() { return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-roles-to-keycloak", true, "sync-keycloak-roles-to-ldap");
@Override
public void onEvent(ProviderEvent event) {
if (event instanceof RealmModel.UserFederationMapperEvent) {
RealmModel.UserFederationMapperEvent mapperEvent = (RealmModel.UserFederationMapperEvent)event;
UserFederationMapperModel mapperModel = mapperEvent.getFederationMapper();
RealmModel realm = mapperEvent.getRealm();
KeycloakSession session = mapperEvent.getSession();
if (mapperModel.getFederationMapperType().equals(PROVIDER_ID)) {
try {
String federationProviderId = mapperModel.getFederationProviderId();
UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderById(federationProviderId, realm);
if (providerModel == null) {
throw new IllegalStateException("Can't find federation provider with ID [" + federationProviderId + "] in realm " + realm.getName());
}
UserFederationProviderFactory ldapFactory = (UserFederationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, providerModel.getProviderName());
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) ldapFactory.getInstance(session, providerModel);
// Sync roles
new RoleLDAPFederationMapper().syncRolesFromLDAP(mapperModel, ldapProvider, realm);
} catch (Exception e) {
logger.warn("Exception during initial sync of roles from LDAP.", e);
}
}
}
}
});
} }
@Override @Override

View file

@ -472,6 +472,12 @@ social.default-scopes.tooltip=The scopes to be sent when asking for authorizatio
key=Key key=Key
stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration. stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration.
# User federation
sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
sync-keycloak-roles-to-ldap=Sync Keycloak Roles To LDAP
sync-ldap-groups-to-keycloak=Sync LDAP Groups To Keycloak
sync-keycloak-groups-to-ldap=Sync Keycloak Groups To LDAP
realms=Realms realms=Realms
realm=Realm realm=Realm

View file

@ -986,7 +986,7 @@ module.controller('UserFederationMapperListCtrl', function($scope, $location, No
}); });
module.controller('UserFederationMapperCtrl', function($scope, realm, provider, mapperTypes, mapper, clients, UserFederationMapper, Notifications, Dialog, $location) { module.controller('UserFederationMapperCtrl', function($scope, realm, provider, mapperTypes, mapper, clients, UserFederationMapper, UserFederationMapperSync, Notifications, Dialog, $location) {
console.log('UserFederationMapperCtrl'); console.log('UserFederationMapperCtrl');
$scope.realm = realm; $scope.realm = realm;
$scope.provider = provider; $scope.provider = provider;
@ -1035,6 +1035,22 @@ module.controller('UserFederationMapperCtrl', function($scope, realm, provider,
}); });
}; };
$scope.triggerFedToKeycloakSync = function() {
triggerMapperSync("fedToKeycloak")
}
$scope.triggerKeycloakToFedSync = function() {
triggerMapperSync("keycloakToFed");
}
function triggerMapperSync(direction) {
UserFederationMapperSync.save({ direction: direction, realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, {}, function(syncResult) {
Notifications.success("Data synced successfully. " + syncResult.status);
}, function() {
Notifications.error("Error during sync of data");
});
}
}); });
module.controller('UserFederationMapperCreateCtrl', function($scope, realm, provider, mapperTypes, clients, UserFederationMapper, Notifications, Dialog, $location) { module.controller('UserFederationMapperCreateCtrl', function($scope, realm, provider, mapperTypes, clients, UserFederationMapper, Notifications, Dialog, $location) {

View file

@ -380,6 +380,10 @@ module.factory('UserFederationMapper', function($resource) {
}); });
}); });
module.factory('UserFederationMapperSync', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mappers/:mapperId/sync');
});
module.factory('UserSessionStats', function($resource) { module.factory('UserSessionStats', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/users/:user/session-stats', { return $resource(authUrl + '/admin/realms/:realm/users/:user/session-stats', {

View file

@ -53,6 +53,8 @@
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm"> <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button kc-save data-ng-disabled="!changed">Save</button> <button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button> <button kc-reset data-ng-disabled="!changed">Cancel</button>
<button class="btn btn-primary" data-ng-click="triggerFedToKeycloakSync()" data-ng-hide="!mapperType.syncConfig.fedToKeycloakSyncSupported" data-ng-disabled="changed">{{:: mapperType.syncConfig.fedToKeycloakSyncMessage | translate}}</button>
<button class="btn btn-primary" data-ng-click="triggerKeycloakToFedSync()" data-ng-hide="!mapperType.syncConfig.keycloakToFedSyncSupported" data-ng-disabled="changed">{{:: mapperType.syncConfig.keycloakToFedSyncMessage | translate}}</button>
</div> </div>
</div> </div>
</form> </form>

View file

@ -1,5 +1,10 @@
package org.keycloak.mappers; package org.keycloak.mappers;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationSyncResult;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
/** /**
@ -7,4 +12,28 @@ import org.keycloak.provider.Provider;
*/ */
public interface UserFederationMapper extends Provider { public interface UserFederationMapper extends Provider {
/**
* Sync data from federation storage to Keycloak. It's useful just if mapper needs some data preloaded from federation storage (For example
* load roles from federation provider and sync them to Keycloak database)
*
* Applicable just if sync is supported (see UserFederationMapperFactory.getSyncConfig() )
*
* @see UserFederationMapperFactory#getSyncConfig()
* @param mapperModel
* @param federationProvider
* @param session
* @param realm
*/
UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm);
/**
* Sync data from Keycloak back to federation storage
*
* @see UserFederationMapperFactory#getSyncConfig()
* @param mapperModel
* @param federationProvider
* @param session
* @param realm
*/
UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm);
} }

View file

@ -1,12 +1,9 @@
package org.keycloak.mappers; package org.keycloak.mappers;
import java.util.List;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -23,6 +20,14 @@ public interface UserFederationMapperFactory extends ProviderFactory<UserFederat
String getDisplayCategory(); String getDisplayCategory();
String getDisplayType(); String getDisplayType();
/**
* Specifies if mapper supports sync data from federation storage to keycloak and viceversa.
* Also specifies messages to be displayed in admin console UI (For example "Sync roles from LDAP" etc)
*
* @return syncConfig representation
*/
UserFederationMapperSyncConfigRepresentation getSyncConfig();
/** /**
* Called when instance of mapperModel is created for this factory through admin endpoint * Called when instance of mapperModel is created for this factory through admin endpoint
* *

View file

@ -29,12 +29,6 @@ public interface RealmModel extends RoleContainerModel {
RealmModel getRealm(); RealmModel getRealm();
} }
interface UserFederationMapperEvent extends ProviderEvent {
UserFederationMapperModel getFederationMapper();
RealmModel getRealm();
KeycloakSession getSession();
}
String getId(); String getId();
String getName(); String getName();

View file

@ -1,33 +0,0 @@
package org.keycloak.models;
/**
* Called during creation or update of UserFederationMapperModel
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserFederationMapperEventImpl implements RealmModel.UserFederationMapperEvent {
private final UserFederationMapperModel mapperModel;
private final RealmModel realm;
private final KeycloakSession session;
public UserFederationMapperEventImpl(UserFederationMapperModel mapperModel, RealmModel realm, KeycloakSession session) {
this.mapperModel = mapperModel;
this.realm = realm;
this.session = session;
}
@Override
public UserFederationMapperModel getFederationMapper() {
return mapperModel;
}
@Override
public RealmModel getRealm() {
return realm;
}
public KeycloakSession getSession() {
return session;
}
}

View file

@ -1,6 +1,5 @@
package org.keycloak.models.jpa; package org.keycloak.models.jpa;
import org.keycloak.Config;
import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.common.enums.SslRequired; import org.keycloak.common.enums.SslRequired;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
@ -19,7 +18,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserFederationMapperEventImpl;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderCreationEventImpl; import org.keycloak.models.UserFederationProviderCreationEventImpl;
import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationProviderModel;
@ -1541,8 +1539,6 @@ public class RealmAdapter implements RealmModel {
this.realm.getUserFederationMappers().add(entity); this.realm.getUserFederationMappers().add(entity);
UserFederationMapperModel mapperModel = entityToModel(entity); UserFederationMapperModel mapperModel = entityToModel(entity);
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapperModel, this, session));
return mapperModel; return mapperModel;
} }
@ -1597,8 +1593,6 @@ public class RealmAdapter implements RealmModel {
entity.getConfig().putAll(mapper.getConfig()); entity.getConfig().putAll(mapper.getConfig());
} }
em.flush(); em.flush();
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapper, this, session));
} }
@Override @Override

View file

@ -22,7 +22,6 @@ import org.keycloak.models.RealmProvider;
import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserFederationMapperEventImpl;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderCreationEventImpl; import org.keycloak.models.UserFederationProviderCreationEventImpl;
import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationProviderModel;
@ -1930,8 +1929,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
updateMongoEntity(); updateMongoEntity();
UserFederationMapperModel mapperModel = entityToModel(entity); UserFederationMapperModel mapperModel = entityToModel(entity);
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapperModel, this, session));
return mapperModel; return mapperModel;
} }
@ -1986,8 +1983,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
entity.getConfig().putAll(mapper.getConfig()); entity.getConfig().putAll(mapper.getConfig());
} }
updateMongoEntity(); updateMongoEntity();
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapper, this, session));
} }
@Override @Override

View file

@ -32,8 +32,11 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderFactory;
import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserFederationSyncResult; import org.keycloak.models.UserFederationSyncResult;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
@ -138,11 +141,13 @@ public class UserFederationProviderResource {
auth.requireManage(); auth.requireManage();
UsersSyncManager syncManager = new UsersSyncManager(); UsersSyncManager syncManager = new UsersSyncManager();
UserFederationSyncResult syncResult = null; UserFederationSyncResult syncResult;
if ("triggerFullSync".equals(action)) { if ("triggerFullSync".equals(action)) {
syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel); syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel);
} else if ("triggerChangedUsersSync".equals(action)) { } else if ("triggerChangedUsersSync".equals(action)) {
syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel); syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel);
} else {
throw new NotFoundException("Unknown action: " + action);
} }
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success(); adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
@ -172,6 +177,7 @@ public class UserFederationProviderResource {
rep.setCategory(mapperFactory.getDisplayCategory()); rep.setCategory(mapperFactory.getDisplayCategory());
rep.setName(mapperFactory.getDisplayType()); rep.setName(mapperFactory.getDisplayType());
rep.setHelpText(mapperFactory.getHelpText()); rep.setHelpText(mapperFactory.getHelpText());
rep.setSyncConfig(mapperFactory.getSyncConfig());
List<ProviderConfigProperty> configProperties = mapperFactory.getConfigProperties(); List<ProviderConfigProperty> configProperties = mapperFactory.getConfigProperties();
for (ProviderConfigProperty prop : configProperties) { for (ProviderConfigProperty prop : configProperties) {
ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation(); ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
@ -307,6 +313,41 @@ public class UserFederationProviderResource {
} }
/**
* Trigger sync of mapper data related to federationMapper (roles, groups, ...)
*
* @return
*/
@POST
@Path("mappers/{id}/sync")
@NoCache
public UserFederationSyncResult syncMapperData(@PathParam("id") String mapperId, @QueryParam("direction") String direction) {
auth.requireManage();
UserFederationMapperModel mapperModel = realm.getUserFederationMapperById(mapperId);
if (mapperModel == null) throw new NotFoundException("Mapper model not found");
UserFederationMapper mapper = session.getProvider(UserFederationMapper.class, mapperModel.getFederationMapperType());
UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderById(mapperModel.getFederationProviderId(), realm);
if (providerModel == null) throw new NotFoundException("Provider model not found");
UserFederationProviderFactory providerFactory = (UserFederationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, providerModel.getProviderName());
UserFederationProvider federationProvider = providerFactory.getInstance(session, providerModel);
logger.infof("Syncing data for mapper '%s' of type '%s'. Direction: %s", mapperModel.getName(), mapperModel.getFederationMapperType(), direction);
UserFederationSyncResult syncResult;
if ("fedToKeycloak".equals(direction)) {
syncResult = mapper.syncDataFromFederationProviderToKeycloak(mapperModel, federationProvider, session, realm);
} else if ("keycloakToFed".equals(direction)) {
syncResult = mapper.syncDataFromKeycloakToFederationProvider(mapperModel, federationProvider, session, realm);
} else {
throw new NotFoundException("Unknown direction: " + direction);
}
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
return syncResult;
}
private void validateModel(UserFederationMapperModel model) { private void validateModel(UserFederationMapperModel model) {
try { try {
UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType()); UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType());

View file

@ -141,6 +141,16 @@ class FederationTestUtils {
} }
} }
public static void syncRolesFromLDAP(RealmModel realm, LDAPFederationProvider ldapProvider, UserFederationProviderModel providerModel) {
RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper();
UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "realmRolesMapper");
roleMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, ldapProvider.getSession(), realm);
mapperModel = realm.getUserFederationMapperByName(providerModel.getId(), "financeRolesMapper");
roleMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, ldapProvider.getSession(), realm);
}
public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) { public static void removeAllLDAPUsers(LDAPFederationProvider ldapProvider, RealmModel realm) {
LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore(); LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm); LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);

View file

@ -88,6 +88,9 @@ public class LDAPRoleMappingsTest {
FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole1"); FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole1");
FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole2"); FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole2");
FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "financeRolesMapper", "financeRole1"); FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "financeRolesMapper", "financeRole1");
// Sync LDAP roles to Keycloak DB
FederationTestUtils.syncRolesFromLDAP(appRealm, ldapFedProvider, ldapModel);
} }
}); });