diff --git a/docs/UAA-APIs.rst b/docs/UAA-APIs.rst index 74bf6d2ef7a..4bc436b353d 100644 --- a/docs/UAA-APIs.rst +++ b/docs/UAA-APIs.rst @@ -1390,7 +1390,8 @@ Fields *Available Fields* :: skipSSLVerification boolean Optional Set to true if you wish to skip SSL certificate verification emailDomain List Optional List of email domains associated with the LDAP provider for the purpose of associating users to the correct origin upon invitation. If null or empty list, no invitations are accepted. Wildcards supported. attributeMappings Map Optional List of UAA attributes mapped to attributes from LDAP. Currently we support mapping given_name, family_name, email, phone_number and external_groups. - externalGroupsWhitelist List Optional List of external groups (`DN` distinguished names`) that can be included in the ID Token if the `roles` scope is requested. See `UAA-LDAP.md UAA-LDAP.md`_ for more information + externalGroupsWhitelist List Optional List of external groups that can be included in the ID Token if the `roles` scope is requested. By default matched against the bare `cn` of each LDAP group; set `includeExternalGroupDn` to `true` to also allow matching on the group's full `DN`. See `UAA-LDAP.md UAA-LDAP.md`_ for more information + includeExternalGroupDn boolean Optional Defaults to false. When true, the full `DN` of each LDAP group is added as an additional candidate value (alongside the existing `cn` value) for `externalGroupsWhitelist` matching and the `/userinfo` `roles` field. See `UAA-LDAP.md UAA-LDAP.md`_ for more information providerDescription String Optional Human readable name/description of this provider Curl Example POST (Creating a SAML provider):: diff --git a/docs/UAA-Configuration-Reference.md b/docs/UAA-Configuration-Reference.md index 7fc4a0a401f..967911c1e1b 100644 --- a/docs/UAA-Configuration-Reference.md +++ b/docs/UAA-Configuration-Reference.md @@ -294,6 +294,9 @@ or `$CLOUDFOUNDRY_CONFIG_PATH/uaa.yml`. | `ldap.groups.groupSearchFilter` | —| Group membership filter| | `ldap.groups.maxSearchDepth` | `10`| Max nested group depth| | `ldap.groups.autoAdd` | —| Auto-add LDAP groups| +| `ldap.groups.groupRoleAttribute` | `description`| Attribute holding the scope name(s) for `groups-as-scopes`| +| `ldap.externalGroupsWhitelist` | —| External group names/patterns allowed into the `roles` field| +| `ldap.includeExternalGroupDn` | `false`| Also match external groups by full DN| ### Encryption @@ -2686,6 +2689,60 @@ When `true`, LDAP groups are automatically created as UAA groups if they don't e --- +### `ldap.groups.groupRoleAttribute` + +**Default:** `description` +**Source:** YAML config +**Type:** `String` + +Used with `groups-as-scopes` (`ldap.groups.file: ldap/ldap-groups-as-scopes.xml`), defines the +LDAP attribute that holds the scope name(s) for a group. Ignored by `groups-map-to-scopes`, where +the group's DN is used instead. + +[Back to table](#ldap) + +--- + +### `ldap.externalGroupsWhitelist` + +**Default:** — (not set, meaning all external groups are allowed) +**Source:** YAML config +**Type:** `List` + +Allowlist of external group names/patterns (wildcards supported) that may be reported as the +user's external groups — used to populate the `/userinfo` endpoint's `roles` field. By default, +each LDAP group the user belongs to is matched by its bare `cn` (e.g. `cf_admin`), not its full +`DN`. Set `ldap.includeExternalGroupDn` to `true` to also allow matching on the group's full `DN`. + +[Back to table](#ldap) + +--- + +### `ldap.includeExternalGroupDn` + +**Default:** `false` +**Source:** YAML config +**Type:** `boolean` + +When `true`, the full `DN` of each LDAP group the user belongs to is added as an additional +candidate value (alongside the existing `cn` value) when matching against +`ldap.externalGroupsWhitelist` and when populating the `/userinfo` endpoint's `roles` field. This +does not replace the `cn`-based value, so existing `cn`-based whitelist entries keep working when +this is enabled. + +**Caution:** this can meaningfully grow the size of the `/userinfo` response and any token that +carries the `roles` claim. Each group now contributes two entries instead of one, and a `DN` is +typically far longer than a bare `cn` (e.g. `cf_admin` vs. +`cn=cf_admin,ou=dev,dc=example,dc=com` — roughly 6x the characters), so the size increase is +driven more by `DN` length than by the doubled entry count. For a user in many or deeply-nested +groups, with a broad `externalGroupsWhitelist` (e.g. `*`), this can add up to a substantial +increase in payload size — worth checking against any downstream token/header size limits before +enabling broadly. + +[Back to table](#ldap) + +--- + ### `encryption.active_key_label` **Default:** — (required) diff --git a/docs/UAA-LDAP.md b/docs/UAA-LDAP.md index d828c70f55f..2523f1fe6fa 100644 --- a/docs/UAA-LDAP.md +++ b/docs/UAA-LDAP.md @@ -684,8 +684,26 @@ In the above example, the user `marissa`'s UAA email always become `generated-m * `ldap.externalGroupsWhitelist` List value, Optional List of external groups that will be included in the ID Token if the `roles` scope is requested. - The list should contain `DN` values for the groups that are associated with the user. - The display name of the group in the ID token will be the taken from the `ldap.groups.groupRoleAttribute` attribute + By default, the list is matched against the bare `cn` of each LDAP group the user belongs to (e.g. `cf_admin`), + not the group's full `DN`. Set `ldap.includeExternalGroupDn` to `true` to also allow matching on the full `DN` + (e.g. `cn=cf_admin,ou=dev,dc=example,dc=com`). + + +* `ldap.includeExternalGroupDn` + boolean value, defaults to `false`. + When `true`, the full `DN` of each LDAP group the user belongs to is added as an additional candidate value + (alongside the existing bare `cn` value) when matching against `ldap.externalGroupsWhitelist` and when + populating the external groups reported via the `/userinfo` endpoint's `roles` field. This does not replace + the `cn`-based value, so existing `cn`-based whitelist entries keep working when this is enabled. + Useful when an external system (for example, a role-to-external-group mapping configured with a group's + full `DN`) needs to match against the `DN` rather than the bare `cn`. +
**Caution:** this can meaningfully grow the size of the `/userinfo` response and any token that carries + the `roles` claim. Each group now contributes two entries instead of one, and a `DN` is typically far longer + than a bare `cn` (e.g. `cf_admin` vs. `cn=cf_admin,ou=dev,dc=example,dc=com` — roughly 6x the characters), so + the size increase is driven more by `DN` length than by the doubled entry count. For a user in many or + deeply-nested groups, with a broad `externalGroupsWhitelist` (e.g. `*`), this can add up to a substantial + increase in payload size — worth checking against any downstream token/header size limits before enabling + broadly. * `ldap.attributeMappings` diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/LdapIdentityProviderDefinition.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/LdapIdentityProviderDefinition.java index 38a6698349d..8cb6f57dc9b 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/LdapIdentityProviderDefinition.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/LdapIdentityProviderDefinition.java @@ -49,6 +49,7 @@ public class LdapIdentityProviderDefinition extends ExternalIdentityProviderDefi public static final String LDAP_EMAIL_DOMAIN = LDAP_PREFIX + EMAIL_DOMAIN_ATTR; public static final String LDAP_STORE_CUSTOM_ATTRIBUTES = LDAP_PREFIX + STORE_CUSTOM_ATTRIBUTES_NAME; public static final String LDAP_EXTERNAL_GROUPS_WHITELIST = LDAP_PREFIX + "externalGroupsWhitelist"; + public static final String LDAP_INCLUDE_EXTERNAL_GROUP_DN = LDAP_PREFIX + "includeExternalGroupDn"; public static final String LDAP_GROUP_FILE_GROUPS_AS_SCOPES = "ldap/ldap-groups-as-scopes.xml"; public static final String LDAP_GROUP_FILE_GROUPS_MAP_TO_SCOPES = "ldap/ldap-groups-map-to-scopes.xml"; public static final String LDAP_GROUP_FILE_GROUPS_NULL_XML = "ldap/ldap-groups-null.xml"; @@ -75,7 +76,7 @@ public class LdapIdentityProviderDefinition extends ExternalIdentityProviderDefi List.of("ldap/ldap-groups-as-scopes.xml", "ldap/ldap-groups-map-to-scopes.xml", "ldap/ldap-groups-null.xml", "ldap/ldap-groups-populator.xml"); - public static final List LDAP_PROPERTY_NAMES = List.of(LDAP_ATTRIBUTE_MAPPINGS, LDAP_BASE_LOCAL_PASSWORD_COMPARE, LDAP_BASE_MAIL_ATTRIBUTE_NAME, LDAP_BASE_MAIL_SUBSTITUTE, LDAP_BASE_MAIL_SUBSTITUTE_OVERRIDES_LDAP, LDAP_BASE_PASSWORD, LDAP_BASE_PASSWORD_ATTRIBUTE_NAME, LDAP_BASE_PASSWORD_ENCODER, LDAP_BASE_REFERRAL, LDAP_BASE_SEARCH_BASE, LDAP_BASE_SEARCH_FILTER, LDAP_BASE_URL, LDAP_BASE_USER_DN, LDAP_BASE_USER_DN_PATTERN, LDAP_BASE_USER_DN_PATTERN_DELIMITER, LDAP_EMAIL_DOMAIN, LDAP_EXTERNAL_GROUPS_WHITELIST, LDAP_GROUPS_AUTO_ADD, LDAP_GROUPS_FILE, LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE, LDAP_GROUPS_GROUP_SEARCH_FILTER, LDAP_GROUPS_IGNORE_PARTIAL_RESULT_EXCEPTION, LDAP_GROUPS_MAX_SEARCH_DEPTH, LDAP_GROUPS_SEARCH_BASE, LDAP_GROUPS_SEARCH_SUBTREE, LDAP_PROFILE_FILE, LDAP_SSL_SKIPVERIFICATION, LDAP_SSL_TLS); + public static final List LDAP_PROPERTY_NAMES = List.of(LDAP_ATTRIBUTE_MAPPINGS, LDAP_BASE_LOCAL_PASSWORD_COMPARE, LDAP_BASE_MAIL_ATTRIBUTE_NAME, LDAP_BASE_MAIL_SUBSTITUTE, LDAP_BASE_MAIL_SUBSTITUTE_OVERRIDES_LDAP, LDAP_BASE_PASSWORD, LDAP_BASE_PASSWORD_ATTRIBUTE_NAME, LDAP_BASE_PASSWORD_ENCODER, LDAP_BASE_REFERRAL, LDAP_BASE_SEARCH_BASE, LDAP_BASE_SEARCH_FILTER, LDAP_BASE_URL, LDAP_BASE_USER_DN, LDAP_BASE_USER_DN_PATTERN, LDAP_BASE_USER_DN_PATTERN_DELIMITER, LDAP_EMAIL_DOMAIN, LDAP_EXTERNAL_GROUPS_WHITELIST, LDAP_INCLUDE_EXTERNAL_GROUP_DN, LDAP_GROUPS_AUTO_ADD, LDAP_GROUPS_FILE, LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE, LDAP_GROUPS_GROUP_SEARCH_FILTER, LDAP_GROUPS_IGNORE_PARTIAL_RESULT_EXCEPTION, LDAP_GROUPS_MAX_SEARCH_DEPTH, LDAP_GROUPS_SEARCH_BASE, LDAP_GROUPS_SEARCH_SUBTREE, LDAP_PROFILE_FILE, LDAP_SSL_SKIPVERIFICATION, LDAP_SSL_TLS); public static final Map> LDAP_PROPERTY_TYPES = new HashMap<>(); @@ -97,6 +98,7 @@ public class LdapIdentityProviderDefinition extends ExternalIdentityProviderDefi LDAP_PROPERTY_TYPES.put(LDAP_BASE_USER_DN_PATTERN_DELIMITER, String.class); LDAP_PROPERTY_TYPES.put(LDAP_EMAIL_DOMAIN, List.class); LDAP_PROPERTY_TYPES.put(LDAP_EXTERNAL_GROUPS_WHITELIST, List.class); + LDAP_PROPERTY_TYPES.put(LDAP_INCLUDE_EXTERNAL_GROUP_DN, Boolean.class); LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_AUTO_ADD, Boolean.class); LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_FILE, String.class); LDAP_PROPERTY_TYPES.put(LDAP_GROUPS_GROUP_ROLE_ATTRIBUTE, String.class); @@ -138,6 +140,7 @@ public class LdapIdentityProviderDefinition extends ExternalIdentityProviderDefi private Boolean groupSearchSubTree = true; private int maxGroupSearchDepth = 10; private String groupRoleAttribute; + private Boolean includeExternalGroupDn = false; private String tlsConfiguration = LDAP_TLS_NONE; @@ -356,6 +359,14 @@ public void setGroupRoleAttribute(String groupRoleAttribute) { this.groupRoleAttribute = groupRoleAttribute; } + public Boolean isIncludeExternalGroupDn() { + return includeExternalGroupDn; + } + + public void setIncludeExternalGroupDn(Boolean includeExternalGroupDn) { + this.includeExternalGroupDn = includeExternalGroupDn; + } + @JsonIgnore public Boolean isConfigured() { return StringUtils.hasText(getBaseUrl()); @@ -485,6 +496,9 @@ public boolean equals(Object o) { if (!Objects.equals(groupSearchSubTree, that.groupSearchSubTree)) { return false; } + if (!Objects.equals(includeExternalGroupDn, that.includeExternalGroupDn)) { + return false; + } return Objects.equals(groupRoleAttribute, that.groupRoleAttribute); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java index c485f418504..0b9025c759c 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManager.java @@ -24,6 +24,7 @@ import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.ldap.ExtendedLdapUserDetails; import org.cloudfoundry.identity.uaa.provider.ldap.extension.LdapAuthority; +import org.cloudfoundry.identity.uaa.provider.ldap.extension.SpringSecurityLdapTemplate; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.util.ObjectUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; @@ -97,12 +98,13 @@ protected List getExternalUserAuthorities(UserDetails request, ExternalA IdentityProvider provider = getProviderProvisioning().retrieveByOrigin(authenticationData.getOrigin(), IdentityZoneHolder.get().getId()); LdapIdentityProviderDefinition ldapIdentityProviderDefinition = ObjectUtils.castInstance(provider.getConfig(), LdapIdentityProviderDefinition.class); List externalWhiteList = ldapIdentityProviderDefinition.getExternalGroupsWhitelist(); - result = new ArrayList<>(retainAllMatches(getAuthoritiesAsNames(request.getAuthorities()), externalWhiteList)); + boolean includeDn = Boolean.TRUE.equals(ldapIdentityProviderDefinition.isIncludeExternalGroupDn()); + result = new ArrayList<>(retainAllMatches(getAuthoritiesAsNames(request.getAuthorities(), includeDn), externalWhiteList)); } return result; } - protected Set getAuthoritiesAsNames(Collection authorities) { + protected Set getAuthoritiesAsNames(Collection authorities, boolean includeDn) { Set result = new HashSet<>(); authorities = new LinkedList<>(authorities != null ? authorities : emptyList()); for (GrantedAuthority a : authorities) { @@ -111,6 +113,12 @@ protected Set getAuthoritiesAsNames(Collection ldap definition.setExternalGroupsWhitelist((List) ldapConfig.get(LdapIdentityProviderDefinition.LDAP_EXTERNAL_GROUPS_WHITELIST)); } + if (ldapConfig.get(LdapIdentityProviderDefinition.LDAP_INCLUDE_EXTERNAL_GROUP_DN) != null) { + definition.setIncludeExternalGroupDn((Boolean) ldapConfig.get(LdapIdentityProviderDefinition.LDAP_INCLUDE_EXTERNAL_GROUP_DN)); + } + if (ldapConfig.get(LdapIdentityProviderDefinition.LDAP_ATTRIBUTE_MAPPINGS) != null) { definition.setAttributeMappings((Map) ldapConfig.get(LdapIdentityProviderDefinition.LDAP_ATTRIBUTE_MAPPINGS)); } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java index 8a6f51a78ea..8f8bd87f687 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/authentication/manager/LdapLoginAuthenticationManagerTests.java @@ -9,6 +9,7 @@ import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.ldap.extension.ExtendedLdapUserImpl; import org.cloudfoundry.identity.uaa.provider.ldap.extension.LdapAuthority; +import org.cloudfoundry.identity.uaa.provider.ldap.extension.SpringSecurityLdapTemplate; import org.cloudfoundry.identity.uaa.user.UaaAuthority; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; @@ -272,6 +273,89 @@ void group_white_list_with_wildcard() { assertThat(am.getExternalUserAuthorities(authDetails, authenticationData)).containsExactlyInAnyOrder("ldap.role.1.a", "ldap.role.1.b", "ldap.role.1", "ldap.role.2.a", "ldap.role.2.b", "ldap.role.2"); } + @Test + void group_white_list_does_not_match_dn_by_default() { + UaaUser user = getUaaUser(); + ExtendedLdapUserImpl authDetails = + getAuthDetails( + user.getEmail(), + user.getGivenName(), + user.getFamilyName(), + user.getPhoneNumber(), + new AttributeInfo(UAA_MANAGER, new String[]{KARI_THE_ANT_EATER, JOHN_THE_SLOTH}), + new AttributeInfo(COST_CENTER, new String[]{DENVER_CO}) + ); + Map role1 = new HashMap<>(); + role1.put("cn", new String[]{"ldap.role.1"}); + role1.put(SpringSecurityLdapTemplate.DN_KEY, new String[]{"cn=role1,ou=test,ou=com"}); + authDetails.setAuthorities( + Collections.singletonList(new LdapAuthority("role1", "cn=role1,ou=test,ou=com", role1)) + ); + + final ExternalAuthenticationDetails authenticationData = ExternalAuthenticationDetails.builder().origin(origin).build(); + + definition.setExternalGroupsWhitelist(Collections.singletonList("cn=role1,ou=test,ou=com")); + assertThat(am.getExternalUserAuthorities(authDetails, authenticationData)).containsExactlyInAnyOrder(); + } + + @Test + void group_white_list_matches_dn_when_include_dn_enabled() { + UaaUser user = getUaaUser(); + ExtendedLdapUserImpl authDetails = + getAuthDetails( + user.getEmail(), + user.getGivenName(), + user.getFamilyName(), + user.getPhoneNumber(), + new AttributeInfo(UAA_MANAGER, new String[]{KARI_THE_ANT_EATER, JOHN_THE_SLOTH}), + new AttributeInfo(COST_CENTER, new String[]{DENVER_CO}) + ); + Map role1 = new HashMap<>(); + role1.put("cn", new String[]{"ldap.role.1"}); + role1.put(SpringSecurityLdapTemplate.DN_KEY, new String[]{"cn=role1,ou=test,ou=com"}); + authDetails.setAuthorities( + Collections.singletonList(new LdapAuthority("role1", "cn=role1,ou=test,ou=com", role1)) + ); + + final ExternalAuthenticationDetails authenticationData = ExternalAuthenticationDetails.builder().origin(origin).build(); + + definition.setIncludeExternalGroupDn(true); + definition.setExternalGroupsWhitelist(Collections.singletonList("cn=role1,ou=test,ou=com")); + assertThat(am.getExternalUserAuthorities(authDetails, authenticationData)).containsExactlyInAnyOrder("cn=role1,ou=test,ou=com"); + } + + @Test + void group_white_list_matches_both_cn_and_dn_when_include_dn_enabled() { + UaaUser user = getUaaUser(); + ExtendedLdapUserImpl authDetails = + getAuthDetails( + user.getEmail(), + user.getGivenName(), + user.getFamilyName(), + user.getPhoneNumber(), + new AttributeInfo(UAA_MANAGER, new String[]{KARI_THE_ANT_EATER, JOHN_THE_SLOTH}), + new AttributeInfo(COST_CENTER, new String[]{DENVER_CO}) + ); + Map role1 = new HashMap<>(); + role1.put("cn", new String[]{"ldap.role.1"}); + role1.put(SpringSecurityLdapTemplate.DN_KEY, new String[]{"cn=role1,ou=test,ou=com"}); + Map role2 = new HashMap<>(); + role2.put("cn", new String[]{"ldap.role.2"}); + role2.put(SpringSecurityLdapTemplate.DN_KEY, new String[]{"cn=role2,ou=test,ou=com"}); + authDetails.setAuthorities( + Arrays.asList( + new LdapAuthority("role1", "cn=role1,ou=test,ou=com", role1), + new LdapAuthority("role2", "cn=role2,ou=test,ou=com", role2) + ) + ); + + final ExternalAuthenticationDetails authenticationData = ExternalAuthenticationDetails.builder().origin(origin).build(); + + definition.setIncludeExternalGroupDn(true); + definition.setExternalGroupsWhitelist(Arrays.asList("cn=role1,ou=test,ou=com", "ldap.role.2")); + assertThat(am.getExternalUserAuthorities(authDetails, authenticationData)).containsExactlyInAnyOrder("cn=role1,ou=test,ou=com", "ldap.role.2"); + } + void test_authentication_attributes(boolean storeUserInfo) { UaaUser user = getUaaUser(); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderDefinitionTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderDefinitionTest.java index 9bad68d29d9..7fc2f1d7731 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderDefinitionTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/ldap/LdapIdentityProviderDefinitionTest.java @@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThat; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; +import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.LDAP_INCLUDE_EXTERNAL_GROUP_DN; import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.LDAP_PROPERTY_TYPES; import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.LDAP_SSL_TLS; import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.LDAP_TLS_EXTERNAL; @@ -48,6 +49,7 @@ class LdapIdentityProviderDefinitionTest { @Test void property_types() { assertThat(LDAP_PROPERTY_TYPES).containsEntry(LDAP_SSL_TLS, String.class); + assertThat(LDAP_PROPERTY_TYPES).containsEntry(LDAP_INCLUDE_EXTERNAL_GROUP_DN, Boolean.class); } @Test @@ -65,6 +67,12 @@ void equals() { ldapIdentityProviderDefinition2.setAddShadowUserOnLogin(true); assertThat(ldapIdentityProviderDefinition2).isEqualTo(ldapIdentityProviderDefinition1); + + ldapIdentityProviderDefinition1.setIncludeExternalGroupDn(true); + assertThat(ldapIdentityProviderDefinition2).isNotEqualTo(ldapIdentityProviderDefinition1); + + ldapIdentityProviderDefinition2.setIncludeExternalGroupDn(true); + assertThat(ldapIdentityProviderDefinition2).isEqualTo(ldapIdentityProviderDefinition1); } @Test @@ -238,6 +246,7 @@ void simple_bind_config() { assertThat(def.getMaxGroupSearchDepth()).isEqualTo(10); assertThat(def.isAutoAddGroups()).isTrue(); assertThat(def.getGroupRoleAttribute()).isNull(); + assertThat(def.isIncludeExternalGroupDn()).isFalse(); } @Test @@ -275,6 +284,7 @@ void search_and_bind_config() { assertThat(def.getMaxGroupSearchDepth()).isEqualTo(10); assertThat(def.isAutoAddGroups()).isTrue(); assertThat(def.getGroupRoleAttribute()).isNull(); + assertThat(def.isIncludeExternalGroupDn()).isFalse(); } @Test @@ -321,6 +331,25 @@ void search_and_bind_with_groups_config() { assertThat(def.getGroupRoleAttribute()).isNull(); } + @Test + void search_and_bind_with_include_external_group_dn_config() { + String config = """ + ldap: + profile: + file: ldap/ldap-search-and-bind.xml + base: + url: 'ldap://localhost:10389/' + mailAttributeName: mail + userDn: 'cn=admin,ou=Users,dc=test,dc=com' + password: 'password' + searchBase: '' + searchFilter: 'cn={0}' + includeExternalGroupDn: true"""; + LdapIdentityProviderDefinition def = LdapUtils.fromConfig(getLdapConfig(config)); + + assertThat(def.isIncludeExternalGroupDn()).isTrue(); + } + @Test void search_and_compare_config() { String config = """ @@ -447,6 +476,16 @@ void set_external_groups_whitelist() { assertThat(def.getExternalGroupsWhitelist()).containsExactlyElementsOf(Collections.singletonList("value")); } + @Test + void set_include_external_group_dn() { + LdapIdentityProviderDefinition def = new LdapIdentityProviderDefinition(); + assertThat(def.isIncludeExternalGroupDn()).isFalse(); + def.setIncludeExternalGroupDn(true); + assertThat(def.isIncludeExternalGroupDn()).isTrue(); + def = JsonUtils.readValue(JsonUtils.writeValueAsString(def), LdapIdentityProviderDefinition.class); + assertThat(def.isIncludeExternalGroupDn()).isTrue(); + } + @Test void set_user_attributes() { LdapIdentityProviderDefinition def = new LdapIdentityProviderDefinition(); diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 21969b0763c..c039730d147 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -99,6 +99,7 @@ zones: # externalGroupsWhitelist: # - admin # - user +# includeExternalGroupDn: false # emailDomain: # - example.com # attributeMappings: @@ -612,6 +613,7 @@ ldap: autoAdd: true externalGroupsWhitelist: - '*' + includeExternalGroupDn: false springdoc: api-docs: diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/AbstractLdapMockMvcTest.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/AbstractLdapMockMvcTest.java index 3f06fa65c07..4fa68238adc 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/AbstractLdapMockMvcTest.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/ldap/AbstractLdapMockMvcTest.java @@ -403,6 +403,40 @@ void external_groups_whitelist() throws Exception { IdentityZoneHolder.clear(); } + @Test + void external_groups_whitelist_by_dn() throws Exception { + assumeTrue("ldap-groups-map-to-scopes.xml, ldap-groups-as-scopes.xml".contains(ldapGroup)); + AuthenticationManager manager = getWebApplicationContext().getBean(DynamicZoneAwareAuthenticationManager.class); + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa3", "ldap3"); + + LdapIdentityProviderDefinition def = provider.getConfig(); + def.addWhiteListedGroup("cn=admins,ou=scopes,dc=test,dc=com"); + provider.setConfig(def); + updateLdapProvider(); + + IdentityZoneHolder.set(zone.getZone().getIdentityZone()); + Authentication auth = manager.authenticate(token); + assertThat(auth).isInstanceOf(UaaAuthentication.class); + UaaAuthentication uaaAuth = (UaaAuthentication) auth; + Set externalGroups = uaaAuth.getExternalGroups(); + //by default, only the bare cn is reported, so a DN-based whitelist entry never matches + assertThat(externalGroups).isEmpty(); + + //enabling includeExternalGroupDn adds the DN as an additional matchable value + def = provider.getConfig(); + def.setIncludeExternalGroupDn(true); + provider.setConfig(def); + updateLdapProvider(); + IdentityZoneHolder.set(zone.getZone().getIdentityZone()); + auth = manager.authenticate(token); + assertThat(auth).isInstanceOf(UaaAuthentication.class); + uaaAuth = (UaaAuthentication) auth; + externalGroups = uaaAuth.getExternalGroups(); + assertThat(externalGroups).containsExactlyInAnyOrder("cn=admins,ou=scopes,dc=test,dc=com"); + + IdentityZoneHolder.clear(); + } + @Test void customUserAttributes() throws Exception { assumeTrue("ldap-groups-map-to-scopes.xml, ldap-groups-as-scopes.xml".contains(ldapGroup)); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 74e3b9daa15..75f1d491bbf 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -274,6 +274,7 @@ static void startLdapContainer() { private final FieldDescriptor ldapUserCompareEncoder = fieldWithPath("config.passwordEncoder").optional("org.cloudfoundry.identity.uaa.provider.ldap.DynamicPasswordComparator").type(STRING).description("Used with `search-and-compare` only. A fully-qualified Java classname to the password encoder. This encoder is used to properly encode user password to match the one in the LDAP directory."); private final FieldDescriptor ldapUserCompareLocal = fieldWithPath("config.localPasswordCompare").optional(null).type(BOOLEAN).description("Set to true if the comparison should be done locally. Setting this value to false implies that rather than retrieving the password, the UAA will run a query to match the password. In order for this query to work, you must know what type of hash/encoding/salt is used for the LDAP password."); private final FieldDescriptor ldapGroupRoleAttribute = fieldWithPath("config.groupRoleAttribute").optional("description").type(STRING).description("Used with `groups-as-scopes`, defines the attribute that holds the scope name(s)."); + private final FieldDescriptor ldapIncludeExternalGroupDn = fieldWithPath("config.includeExternalGroupDn").optional(false).type(BOOLEAN).description("Defaults to false. When true, the full `DN` of each LDAP group is added as an additional candidate value (alongside the existing `cn` value) for `externalGroupsWhitelist` matching and the `/userinfo` `roles` field."); private final FieldDescriptor ldapAttributeMappingFirstname = fieldWithPath("config.attributeMappings.first_name").optional("givenname").type(STRING).description(GIVEN_NAME_DESC); private final FieldDescriptor ldapAttributeMappingLastname = fieldWithPath("config.attributeMappings.family_name").optional("sn").type(STRING).description(FAMILY_NAME_DESC); private final FieldDescriptor ldapAttributeMappingPhone = fieldWithPath("config.attributeMappings.phone_number").optional("telephonenumber").type(STRING).description(PHONE_NUMBER_DESC); @@ -309,6 +310,7 @@ static void startLdapContainer() { ldapUserCompareEncoder, ldapUserCompareLocal, ldapGroupRoleAttribute, + ldapIncludeExternalGroupDn, ATTRIBUTE_MAPPING, ldapAttributeMappingUserName, ldapAttributeMappingFirstname, @@ -348,6 +350,7 @@ static void startLdapContainer() { ldapUserCompareEncoder, ldapUserCompareLocal, ldapGroupRoleAttribute, + ldapIncludeExternalGroupDn, ATTRIBUTE_MAPPING, ldapAttributeMappingUserName, ldapAttributeMappingFirstname, @@ -396,7 +399,8 @@ static void startLdapContainer() { ldapUserCompareEncoder.ignored(), ldapUserCompareLocal.ignored(), ldapGroupRoleAttribute.ignored(), - EXTERNAL_GROUPS_WHITELIST.ignored() + EXTERNAL_GROUPS_WHITELIST.ignored(), + ldapIncludeExternalGroupDn.ignored() }, ALIAS_FIELDS_LDAP_CREATE ) @@ -433,6 +437,7 @@ static void startLdapContainer() { ldapUserCompareEncoder.ignored(), ldapUserCompareLocal.ignored(), ldapGroupRoleAttribute.ignored(), + ldapIncludeExternalGroupDn, ATTRIBUTE_MAPPING, ldapAttributeMappingUserName, ldapAttributeMappingFirstname,