Merge pull request #1949 from mposolda/master
LDAP mappers enhancements
This commit is contained in:
commit
2fe7e2a952
24 changed files with 544 additions and 147 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@ public class UserFederationMapperTypeRepresentation {
|
|||
protected String category;
|
||||
protected String helpText;
|
||||
|
||||
protected UserFederationMapperSyncConfigRepresentation syncConfig;
|
||||
|
||||
protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
|
||||
|
||||
public String getId() {
|
||||
|
@ -46,6 +48,14 @@ public class UserFederationMapperTypeRepresentation {
|
|||
this.helpText = helpText;
|
||||
}
|
||||
|
||||
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||
return syncConfig;
|
||||
}
|
||||
|
||||
public void setSyncConfig(UserFederationMapperSyncConfigRepresentation syncConfig) {
|
||||
this.syncConfig = syncConfig;
|
||||
}
|
||||
|
||||
public List<ConfigPropertyRepresentation> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
|
|
@ -62,6 +62,14 @@ public class LDAPDn {
|
|||
return firstEntry.attrName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string attribute value like "joe" from the DN like "uid=joe,dc=something,dc=org"
|
||||
*/
|
||||
public String getFirstRdnAttrValue() {
|
||||
Entry firstEntry = entries.getFirst();
|
||||
return firstEntry.attrValue;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
|
||||
|
@ -72,6 +80,21 @@ public class LDAPDn {
|
|||
return toString(parentDnEntries);
|
||||
}
|
||||
|
||||
public boolean isDescendantOf(LDAPDn expectedParentDn) {
|
||||
int parentEntriesCount = expectedParentDn.entries.size();
|
||||
|
||||
Deque<Entry> myEntries = new LinkedList<>(this.entries);
|
||||
boolean someRemoved = false;
|
||||
while (myEntries.size() > parentEntriesCount) {
|
||||
myEntries.removeFirst();
|
||||
someRemoved = true;
|
||||
}
|
||||
|
||||
String myEntriesParentStr = toString(myEntries).toLowerCase();
|
||||
String expectedParentDnStr = expectedParentDn.toString().toLowerCase();
|
||||
return someRemoved && myEntriesParentStr.equals(expectedParentDnStr);
|
||||
}
|
||||
|
||||
public void addFirst(String rdnName, String rdnValue) {
|
||||
rdnValue = escape(rdnValue);
|
||||
entries.addFirst(new Entry(rdnName, rdnValue));
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
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.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationSyncResult;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.keycloak.mappers.UserFederationMapperFactory;
|
|||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||
return new UserFederationMapperSyncConfigRepresentation(false, null, false, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
|
|
@ -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.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleContainerModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationSyncResult;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
@ -39,11 +42,14 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
// Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn"
|
||||
public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute";
|
||||
|
||||
// Object classes of the role object.
|
||||
public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
|
||||
|
||||
// Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member"
|
||||
public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute";
|
||||
|
||||
// Object classes of the role object.
|
||||
public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
|
||||
// See docs for MembershipType enum
|
||||
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
|
||||
|
||||
// Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID)
|
||||
public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping";
|
||||
|
@ -53,19 +59,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
|
||||
// See docs for Mode enum
|
||||
public static final String MODE = "mode";
|
||||
|
||||
// See docs for UserRolesRetriever enum
|
||||
public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy";
|
||||
|
||||
// Customized LDAP filter which is added to the whole LDAP query
|
||||
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
|
||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||
syncRolesFromLDAP(mapperModel, ldapProvider, realm);
|
||||
|
||||
Mode mode = getMode(mapperModel);
|
||||
|
||||
// For now, import LDAP role mappings just during create
|
||||
|
@ -89,34 +91,91 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
|
||||
@Override
|
||||
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) {
|
||||
if (!rolesSyncedModels.contains(mapperModel.getId())) {
|
||||
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||
|
||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
||||
// Sync roles from LDAP to Keycloak DB
|
||||
@Override
|
||||
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
||||
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
|
||||
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||
|
||||
// Send query
|
||||
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
||||
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
|
||||
for (LDAPObject ldapRole : ldapRoles) {
|
||||
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
||||
|
||||
if (roleContainer.getRole(roleName) == null) {
|
||||
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
|
||||
roleContainer.addRole(roleName);
|
||||
}
|
||||
@Override
|
||||
public String getStatus() {
|
||||
return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated());
|
||||
}
|
||||
|
||||
rolesSyncedModels.add(mapperModel.getId());
|
||||
};
|
||||
|
||||
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);
|
||||
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
||||
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
|
||||
for (LDAPObject ldapRole : ldapRoles) {
|
||||
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
||||
|
||||
if (roleContainer.getRole(roleName) == null) {
|
||||
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
|
||||
roleContainer.addRole(roleName);
|
||||
syncResult.increaseAdded();
|
||||
} else {
|
||||
syncResult.increaseUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
|
||||
|
||||
|
@ -179,6 +238,15 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
||||
}
|
||||
|
||||
protected MembershipType getMembershipTypeLdapAttribute(UserFederationMapperModel mapperModel) {
|
||||
String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE);
|
||||
return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN;
|
||||
}
|
||||
|
||||
protected String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) {
|
||||
return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
|
||||
}
|
||||
|
||||
protected Collection<String> getRoleObjectClasses(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
|
||||
String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES);
|
||||
if (objectClasses == null) {
|
||||
|
@ -206,6 +274,11 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
return Enum.valueOf(Mode.class, modeString.toUpperCase());
|
||||
}
|
||||
|
||||
private UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(UserFederationMapperModel mapperModel) {
|
||||
String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY);
|
||||
return (strategyString!=null && !strategyString.isEmpty()) ? Enum.valueOf(UserRolesRetrieveStrategy.class, strategyString) : UserRolesRetrieveStrategy.LOAD_ROLES_BY_MEMBER_ATTRIBUTE;
|
||||
}
|
||||
|
||||
public LDAPObject createLDAPRole(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider) {
|
||||
LDAPObject ldapObject = new LDAPObject();
|
||||
String roleNameAttribute = getRoleNameLdapAttribute(mapperModel);
|
||||
|
@ -228,17 +301,23 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
ldapRole = createLDAPRole(mapperModel, roleName, ldapProvider);
|
||||
}
|
||||
|
||||
MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel);
|
||||
|
||||
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
|
||||
|
||||
// Remove membership placeholder if present
|
||||
for (String membership : memberships) {
|
||||
if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
|
||||
memberships.remove(membership);
|
||||
break;
|
||||
if (membershipType == MembershipType.DN) {
|
||||
for (String membership : memberships) {
|
||||
if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
|
||||
memberships.remove(membership);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
memberships.add(ldapUser.getDn().toString());
|
||||
String membership = getMembershipFromUser(ldapUser, membershipType);
|
||||
|
||||
memberships.add(membership);
|
||||
ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
|
||||
|
||||
ldapProvider.getLdapIdentityStore().update(ldapRole);
|
||||
|
@ -246,10 +325,14 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
|
||||
public void deleteRoleMappingInLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, LDAPObject ldapRole) {
|
||||
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
|
||||
memberships.remove(ldapUser.getDn().toString());
|
||||
|
||||
MembershipType membershipType = getMembershipTypeLdapAttribute(mapperModel);
|
||||
String userMembership = getMembershipFromUser(ldapUser, membershipType);
|
||||
|
||||
memberships.remove(userMembership);
|
||||
|
||||
// Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here)
|
||||
if (memberships.size() == 0 && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
||||
if (memberships.size() == 0 && membershipType==MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
||||
memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
|
||||
}
|
||||
|
||||
|
@ -274,11 +357,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
}
|
||||
|
||||
protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
||||
String membershipAttr = getMembershipLdapAttribute(mapperModel);
|
||||
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, ldapUser.getDn().toString());
|
||||
ldapQuery.addWhereCondition(membershipCondition);
|
||||
return ldapQuery.getResultList();
|
||||
UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel);
|
||||
return strategy.getLDAPRoleMappings(this, mapperModel, ldapProvider, ldapUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -295,6 +375,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
|
||||
@Override
|
||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel);
|
||||
strategy.beforeUserLDAPQuery(mapperModel, query);
|
||||
}
|
||||
|
||||
|
||||
|
@ -437,7 +519,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||
Condition roleNameCondition = conditionsBuilder.equal(getRoleNameLdapAttribute(mapperModel), role.getName());
|
||||
Condition membershipCondition = conditionsBuilder.equal(getMembershipLdapAttribute(mapperModel), ldapUser.getDn().toString());
|
||||
String membershipUserAttr = getMembershipFromUser(ldapUser, getMembershipTypeLdapAttribute(mapperModel));
|
||||
Condition membershipCondition = conditionsBuilder.equal(getMembershipLdapAttribute(mapperModel), membershipUserAttr);
|
||||
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
|
||||
LDAPObject ldapRole = ldapQuery.getFirstResult();
|
||||
|
||||
|
@ -462,6 +545,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public enum Mode {
|
||||
/**
|
||||
* All role mappings are retrieved from LDAP and saved into LDAP
|
||||
|
@ -484,4 +568,18 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
*/
|
||||
READ_ONLY
|
||||
}
|
||||
|
||||
|
||||
public enum MembershipType {
|
||||
|
||||
/**
|
||||
* Used if 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,
|
||||
|
||||
/**
|
||||
* Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" )
|
||||
*/
|
||||
UID
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,21 +5,13 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
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.ProviderEvent;
|
||||
import org.keycloak.provider.ProviderEventListener;
|
||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -42,23 +34,36 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
|||
ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN);
|
||||
configProperties.add(roleNameLDAPAttribute);
|
||||
|
||||
ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes",
|
||||
"Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(roleObjectClasses);
|
||||
|
||||
ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
|
||||
"Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ",
|
||||
ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER);
|
||||
configProperties.add(membershipLDAPAttribute);
|
||||
|
||||
ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes",
|
||||
"Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(roleObjectClasses);
|
||||
|
||||
|
||||
List<String> membershipTypes = new LinkedList<>();
|
||||
for (RoleLDAPFederationMapper.MembershipType membershipType : RoleLDAPFederationMapper.MembershipType.values()) {
|
||||
membershipTypes.add(membershipType.toString());
|
||||
}
|
||||
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' . " +
|
||||
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .",
|
||||
ProviderConfigProperty.LIST_TYPE, membershipTypes);
|
||||
configProperties.add(membershipType);
|
||||
|
||||
|
||||
ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_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);
|
||||
configProperties.add(ldapFilter);
|
||||
|
||||
List<String> modes = new LinkedList<String>();
|
||||
|
||||
List<String> modes = new LinkedList<>();
|
||||
for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) {
|
||||
modes.add(mode.toString());
|
||||
}
|
||||
|
@ -69,6 +74,20 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
|||
ProviderConfigProperty.LIST_TYPE, modes);
|
||||
configProperties.add(mode);
|
||||
|
||||
|
||||
List<String> roleRetrievers = new LinkedList<>();
|
||||
for (UserRolesRetrieveStrategy retriever : UserRolesRetrieveStrategy.values()) {
|
||||
roleRetrievers.add(retriever.toString());
|
||||
}
|
||||
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. " +
|
||||
"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 Ldap extension."
|
||||
,
|
||||
ProviderConfigProperty.LIST_TYPE, roleRetrievers);
|
||||
configProperties.add(retriever);
|
||||
|
||||
|
||||
ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping",
|
||||
"If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, "true");
|
||||
configProperties.add(useRealmRolesMappings);
|
||||
|
@ -104,40 +123,9 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
|
|||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
// Sync roles from LDAP to Keycloak DB during creation or update of mapperModel
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
factory.register(new ProviderEventListener() {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||
return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-roles-to-keycloak", true, "sync-keycloak-roles-to-ldap");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
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.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
|
||||
/**
|
||||
* Strategy for how to retrieve LDAP roles of user
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public enum UserRolesRetrieveStrategy {
|
||||
|
||||
|
||||
/**
|
||||
* Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user
|
||||
*/
|
||||
LOAD_ROLES_BY_MEMBER_ATTRIBUTE {
|
||||
|
||||
@Override
|
||||
public List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
||||
LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider);
|
||||
String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel);
|
||||
|
||||
String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel));
|
||||
|
||||
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership);
|
||||
ldapQuery.addWhereCondition(membershipCondition);
|
||||
return ldapQuery.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Roles of user will be retrieved from "memberOf" attribute of our user
|
||||
*/
|
||||
GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE {
|
||||
|
||||
@Override
|
||||
public List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
||||
Set<String> memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF);
|
||||
if (memberOfValues == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<LDAPObject> roles = new LinkedList<>();
|
||||
LDAPDn parentDn = LDAPDn.fromString(roleMapper.getRolesDn(mapperModel));
|
||||
|
||||
for (String roleDn : memberOfValues) {
|
||||
LDAPDn roleDN = LDAPDn.fromString(roleDn);
|
||||
if (roleDN.isDescendantOf(parentDn)) {
|
||||
LDAPObject role = new LDAPObject();
|
||||
role.setDn(roleDN);
|
||||
|
||||
String firstDN = roleDN.getFirstRdnAttrName();
|
||||
if (firstDN.equalsIgnoreCase(roleMapper.getRoleNameLdapAttribute(mapperModel))) {
|
||||
role.setRdnAttributeName(firstDN);
|
||||
role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue());
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF);
|
||||
query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user.
|
||||
* The query will be able to retrieve memberships recursively
|
||||
* (Assume "role1" has member "role2" and role2 has member "johnuser". Then searching for roles of "johnuser" will return both "role1" and "role2" )
|
||||
*
|
||||
* This is using AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers
|
||||
*/
|
||||
LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY {
|
||||
|
||||
@Override
|
||||
public List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
||||
LDAPQuery ldapQuery = roleMapper.createRoleQuery(mapperModel, ldapProvider);
|
||||
String membershipAttr = roleMapper.getMembershipLdapAttribute(mapperModel);
|
||||
membershipAttr = membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN;
|
||||
String userMembership = roleMapper.getMembershipFromUser(ldapUser, roleMapper.getMembershipTypeLdapAttribute(mapperModel));
|
||||
|
||||
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership);
|
||||
ldapQuery.addWhereCondition(membershipCondition);
|
||||
return ldapQuery.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
public abstract List<LDAPObject> getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser);
|
||||
|
||||
public abstract void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
|
||||
|
||||
}
|
|
@ -18,5 +18,14 @@ public class LDAPDnTest {
|
|||
Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString());
|
||||
|
||||
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
|
||||
|
||||
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=keycloak, dc=org")));
|
||||
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=org")));
|
||||
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("DC=keycloak, DC=org")));
|
||||
Assert.assertFalse(dn.isDescendantOf(LDAPDn.fromString("dc=keycloakk, dc=org")));
|
||||
Assert.assertFalse(dn.isDescendantOf(dn));
|
||||
|
||||
Assert.assertEquals("uid", dn.getFirstRdnAttrName());
|
||||
Assert.assertEquals("Johny\\,Depp", dn.getFirstRdnAttrValue());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -472,6 +472,12 @@ social.default-scopes.tooltip=The scopes to be sent when asking for authorizatio
|
|||
key=Key
|
||||
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
|
||||
realm=Realm
|
||||
|
||||
|
|
|
@ -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');
|
||||
$scope.realm = realm;
|
||||
$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) {
|
||||
|
|
|
@ -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) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/users/:user/session-stats', {
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
<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-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>
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
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;
|
||||
|
||||
/**
|
||||
|
@ -7,4 +12,28 @@ import org.keycloak.provider.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);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
package org.keycloak.mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -23,6 +20,14 @@ public interface UserFederationMapperFactory extends ProviderFactory<UserFederat
|
|||
String getDisplayCategory();
|
||||
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
|
||||
*
|
||||
|
|
|
@ -87,6 +87,8 @@ public class LDAPConstants {
|
|||
public static final String CREATE_TIMESTAMP = "createTimestamp";
|
||||
public static final String MODIFY_TIMESTAMP = "modifyTimestamp";
|
||||
|
||||
public static final String LDAP_MATCHING_RULE_IN_CHAIN = ":1.2.840.113556.1.4.1941:";
|
||||
|
||||
public static String getUuidAttributeName(String vendor) {
|
||||
if (vendor != null) {
|
||||
switch (vendor) {
|
||||
|
|
|
@ -29,12 +29,6 @@ public interface RealmModel extends RoleContainerModel {
|
|||
RealmModel getRealm();
|
||||
}
|
||||
|
||||
interface UserFederationMapperEvent extends ProviderEvent {
|
||||
UserFederationMapperModel getFederationMapper();
|
||||
RealmModel getRealm();
|
||||
KeycloakSession getSession();
|
||||
}
|
||||
|
||||
String getId();
|
||||
|
||||
String getName();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package org.keycloak.models.jpa;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
|
@ -19,7 +18,6 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserFederationMapperEventImpl;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProviderCreationEventImpl;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
|
@ -1541,8 +1539,6 @@ public class RealmAdapter implements RealmModel {
|
|||
this.realm.getUserFederationMappers().add(entity);
|
||||
UserFederationMapperModel mapperModel = entityToModel(entity);
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapperModel, this, session));
|
||||
|
||||
return mapperModel;
|
||||
}
|
||||
|
||||
|
@ -1597,8 +1593,6 @@ public class RealmAdapter implements RealmModel {
|
|||
entity.getConfig().putAll(mapper.getConfig());
|
||||
}
|
||||
em.flush();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapper, this, session));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -22,7 +22,6 @@ import org.keycloak.models.RealmProvider;
|
|||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.RequiredCredentialModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserFederationMapperEventImpl;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProviderCreationEventImpl;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
|
@ -1930,8 +1929,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
updateMongoEntity();
|
||||
UserFederationMapperModel mapperModel = entityToModel(entity);
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapperModel, this, session));
|
||||
|
||||
return mapperModel;
|
||||
}
|
||||
|
||||
|
@ -1986,8 +1983,6 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
entity.getConfig().putAll(mapper.getConfig());
|
||||
}
|
||||
updateMongoEntity();
|
||||
|
||||
session.getKeycloakSessionFactory().publish(new UserFederationMapperEventImpl(mapper, this, session));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -32,8 +32,11 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationProviderFactory;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.models.UserFederationSyncResult;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
@ -138,11 +141,13 @@ public class UserFederationProviderResource {
|
|||
auth.requireManage();
|
||||
|
||||
UsersSyncManager syncManager = new UsersSyncManager();
|
||||
UserFederationSyncResult syncResult = null;
|
||||
UserFederationSyncResult syncResult;
|
||||
if ("triggerFullSync".equals(action)) {
|
||||
syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel);
|
||||
} else if ("triggerChangedUsersSync".equals(action)) {
|
||||
syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel);
|
||||
} else {
|
||||
throw new NotFoundException("Unknown action: " + action);
|
||||
}
|
||||
|
||||
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
|
||||
|
@ -172,6 +177,7 @@ public class UserFederationProviderResource {
|
|||
rep.setCategory(mapperFactory.getDisplayCategory());
|
||||
rep.setName(mapperFactory.getDisplayType());
|
||||
rep.setHelpText(mapperFactory.getHelpText());
|
||||
rep.setSyncConfig(mapperFactory.getSyncConfig());
|
||||
List<ProviderConfigProperty> configProperties = mapperFactory.getConfigProperties();
|
||||
for (ProviderConfigProperty prop : configProperties) {
|
||||
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) {
|
||||
try {
|
||||
UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType());
|
||||
|
|
|
@ -684,10 +684,11 @@ public class FederationProvidersIntegrationTest {
|
|||
session.users().searchForUser("user5@email.org", appRealm);
|
||||
FederationTestUtils.assertUserImported(session.userStorage(), appRealm, "username5", "John5", "Doel5", "user5@email.org", "125");
|
||||
|
||||
session.users().searchForUser("user6@email.org", appRealm);
|
||||
session.users().searchForUser("John6 Doel6", appRealm);
|
||||
FederationTestUtils.assertUserImported(session.userStorage(), appRealm, "username6", "John6", "Doel6", "user6@email.org", "126");
|
||||
|
||||
session.users().searchForUser("user7@email.org", appRealm);
|
||||
session.users().searchForUser("John7 Doel7", appRealm);
|
||||
Assert.assertNull(session.userStorage().getUserByUsername("username7", appRealm));
|
||||
|
||||
// Remove custom filter
|
||||
|
|
|
@ -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) {
|
||||
LDAPIdentityStore ldapStore = ldapProvider.getLdapIdentityStore();
|
||||
LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
|
||||
|
|
|
@ -88,6 +88,9 @@ public class LDAPRoleMappingsTest {
|
|||
FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole1");
|
||||
FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole2");
|
||||
FederationTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "financeRolesMapper", "financeRole1");
|
||||
|
||||
// Sync LDAP roles to Keycloak DB
|
||||
FederationTestUtils.syncRolesFromLDAP(appRealm, ldapFedProvider, ldapModel);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue