diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java index a7cf098702..be5e6b9d94 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java @@ -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 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)); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java index a62cc5ec24..7eac273ca8 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java @@ -56,6 +56,9 @@ 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"; @@ -184,10 +187,10 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { protected MembershipType getMembershipTypeLdapAttribute(UserFederationMapperModel mapperModel) { String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE); - return membershipType!=null ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN; + return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN; } - private String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) { + protected String getMembershipFromUser(LDAPObject ldapUser, MembershipType membershipType) { return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName()); } @@ -218,6 +221,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); @@ -296,14 +304,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { } protected List getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { - LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider); - String membershipAttr = getMembershipLdapAttribute(mapperModel); - - String userMembership = getMembershipFromUser(ldapUser, getMembershipTypeLdapAttribute(mapperModel)); - - Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership); - ldapQuery.addWhereCondition(membershipCondition); - return ldapQuery.getResultList(); + UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel); + return strategy.getLDAPRoleMappings(this, mapperModel, ldapProvider, ldapUser); } @Override @@ -320,6 +322,8 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper { @Override public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) { + UserRolesRetrieveStrategy strategy = getUserRolesRetrieveStrategy(mapperModel); + strategy.beforeUserLDAPQuery(mapperModel, query); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java index b244d2286e..5cc2997a4b 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java @@ -52,22 +52,25 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER); configProperties.add(membershipLDAPAttribute); + List 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' ))", + "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 ')'", ProviderConfigProperty.STRING_TYPE, null); configProperties.add(ldapFilter); + List modes = new LinkedList<>(); for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) { modes.add(mode.toString()); @@ -79,6 +82,20 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe ProviderConfigProperty.LIST_TYPE, modes); configProperties.add(mode); + + List 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 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); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java new file mode 100644 index 0000000000..dd7cd317c2 --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserRolesRetrieveStrategy.java @@ -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 Marek Posolda + */ +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 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 getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) { + Set memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF); + if (memberOfValues == null) { + return Collections.emptyList(); + } + + List 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 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 getLDAPRoleMappings(RoleLDAPFederationMapper roleMapper, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser); + + public abstract void beforeUserLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query); + +} diff --git a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java index 77bc4ceb60..5631573281 100644 --- a/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java +++ b/federation/ldap/src/test/java/org/keycloak/federation/ldap/idm/model/LDAPDnTest.java @@ -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()); } } diff --git a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java index 79bd7b51d8..2d403f373e 100644 --- a/model/api/src/main/java/org/keycloak/models/LDAPConstants.java +++ b/model/api/src/main/java/org/keycloak/models/LDAPConstants.java @@ -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) {