From 98f0a2120ea2b8305b72b8179b8e82fcaf7ea1f7 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Sat, 3 Feb 2024 04:39:51 +0700 Subject: [PATCH] Add ActiveDirectoryLdapAuthenticationProvider#setAuthoritiesPopulator Closes gh-4490 --- ...veDirectoryLdapAuthenticationProvider.java | 62 ++++++++--------- ...ltActiveDirectoryAuthoritiesPopulator.java | 67 +++++++++++++++++++ 2 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 ldap/src/main/java/org/springframework/security/ldap/authentication/ad/DefaultActiveDirectoryAuthoritiesPopulator.java diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java index d04e13d7b4..18bc258f88 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,9 @@ package org.springframework.security.ldap.authentication.ad; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; -import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,7 +36,6 @@ import org.springframework.core.log.LogMessage; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.ldap.CommunicationException; import org.springframework.ldap.core.DirContextOperations; -import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.support.DefaultDirObjectFactory; import org.springframework.ldap.support.LdapUtils; import org.springframework.security.authentication.AccountExpiredException; @@ -50,11 +46,10 @@ import org.springframework.security.authentication.InternalAuthenticationService import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.ldap.SpringSecurityLdapTemplate; import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -72,9 +67,9 @@ import org.springframework.util.StringUtils; *

* The user authorities are obtained from the data contained in the {@code memberOf} * attribute. - * + *

*

Active Directory Sub-Error Codes

- * + *

* When an authentication fails, resulting in a standard LDAP 49 error code, Active * Directory also supplies its own sub-error codes within the error message. These will be * used to provide additional log information on why an authentication has failed. Typical @@ -90,13 +85,14 @@ import org.springframework.util.StringUtils; *

  • 773 - user must reset password
  • *
  • 775 - account locked
  • * - * + *

    * If you set the {@link #setConvertSubErrorCodesToExceptions(boolean) * convertSubErrorCodesToExceptions} property to {@code true}, the codes will also be used * to control the exception raised. * * @author Luke Taylor * @author Rob Winch + * @author Roman Zabaluev * @since 3.1 */ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLdapAuthenticationProvider { @@ -135,20 +131,23 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda // Only used to allow tests to substitute a mock LdapContext ContextFactory contextFactory = new ContextFactory(); + private LdapAuthoritiesPopulator authoritiesPopulator = new DefaultActiveDirectoryAuthoritiesPopulator(); + /** - * @param domain the domain name (may be null or empty) + * @param domain the domain name (can be null or empty) * @param url an LDAP url (or multiple URLs) - * @param rootDn the root DN (may be null or empty) + * @param rootDn the root DN (can be null or empty) */ public ActiveDirectoryLdapAuthenticationProvider(String domain, String url, String rootDn) { Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty"); this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; this.url = url; this.rootDn = StringUtils.hasText(rootDn) ? rootDn.toLowerCase() : null; + this.setAuthoritiesPopulator(this.authoritiesPopulator); } /** - * @param domain the domain name (may be null or empty) + * @param domain the domain name (can be null or empty) * @param url an LDAP url (or multiple URLs) */ public ActiveDirectoryLdapAuthenticationProvider(String domain, String url) { @@ -156,6 +155,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda this.domain = StringUtils.hasText(domain) ? domain.toLowerCase() : null; this.url = url; this.rootDn = (this.domain != null) ? rootDnFromDomain(this.domain) : null; + this.setAuthoritiesPopulator(this.authoritiesPopulator); } @Override @@ -179,26 +179,10 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda } } - /** - * Creates the user authority list from the values of the {@code memberOf} attribute - * obtained from the user's Active Directory entry. - */ @Override protected Collection loadUserAuthorities(DirContextOperations userData, String username, String password) { - String[] groups = userData.getStringAttributes("memberOf"); - if (groups == null) { - this.logger.debug("No values for 'memberOf' attribute."); - return AuthorityUtils.NO_AUTHORITIES; - } - if (this.logger.isDebugEnabled()) { - this.logger.debug("'memberOf' attribute values: " + Arrays.asList(groups)); - } - List authorities = new ArrayList<>(groups.length); - for (String group : groups) { - authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue())); - } - return authorities; + return this.authoritiesPopulator.getGrantedAuthorities(userData, username); } private DirContext bindAsUser(String username, String password) { @@ -332,14 +316,14 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda + "' does not contain the domain, and no domain has been configured"); throw badCredentials(); } - return rootDnFromDomain(bindPrincipal.substring(atChar + 1, bindPrincipal.length())); + return rootDnFromDomain(bindPrincipal.substring(atChar + 1)); } private String rootDnFromDomain(String domain) { String[] tokens = StringUtils.tokenizeToStringArray(domain, "."); StringBuilder root = new StringBuilder(); for (String token : tokens) { - if (root.length() > 0) { + if (!root.isEmpty()) { root.append(','); } root.append("dc=").append(token); @@ -379,7 +363,6 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda * Defaults to: {@code (&(objectClass=user)(userPrincipalName={0}))} *

    * @param searchFilter the filter string - * * @since 3.2.6 */ public void setSearchFilter(String searchFilter) { @@ -397,6 +380,19 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda this.contextEnvironmentProperties = new Hashtable<>(environment); } + /** + * Set the strategy for obtaining the authorities for a given user after they've been + * authenticated. Consider adjusting this if you require a custom authorities mapping + * algorithm different from a default one. The default value is + * DefaultActiveDirectoryAuthoritiesPopulator. + * @param authoritiesPopulator authorities population strategy + * @since 6.3 + */ + public void setAuthoritiesPopulator(LdapAuthoritiesPopulator authoritiesPopulator) { + Assert.notNull(authoritiesPopulator, "An LdapAuthoritiesPopulator must be supplied"); + this.authoritiesPopulator = authoritiesPopulator; + } + static class ContextFactory { DirContext createContext(Hashtable env) throws NamingException { diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/DefaultActiveDirectoryAuthoritiesPopulator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/DefaultActiveDirectoryAuthoritiesPopulator.java new file mode 100644 index 0000000000..6e179f2fe3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/DefaultActiveDirectoryAuthoritiesPopulator.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.authentication.ad; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; + +/** + * The default strategy for obtaining user role information from the active directory. + * Creates the user authority list from the values of the {@code memberOf} attribute + * obtained from the user's Active Directory entry. + * + * @author Luke Taylor + * @author Roman Zabaluev + */ +public final class DefaultActiveDirectoryAuthoritiesPopulator implements LdapAuthoritiesPopulator { + + private final Log logger = LogFactory.getLog(getClass()); + + @Override + public Collection getGrantedAuthorities(DirContextOperations userData, + String username) { + String[] groups = userData.getStringAttributes("memberOf"); + if (groups == null) { + this.logger.debug("No values for 'memberOf' attribute."); + return AuthorityUtils.NO_AUTHORITIES; + } + if (this.logger.isDebugEnabled()) { + this.logger.debug("'memberOf' attribute values: " + Arrays.asList(groups)); + } + + List authorities = new ArrayList<>(groups.length); + + for (String group : groups) { + authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue())); + } + + return authorities; + } + +}