Add ActiveDirectoryLdapAuthenticationProvider#setAuthoritiesPopulator

Closes gh-4490
This commit is contained in:
Roman Zabaluev 2024-02-03 04:39:51 +07:00 committed by Josh Cummings
parent aafa4dd614
commit 98f0a2120e
2 changed files with 96 additions and 33 deletions

View File

@ -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;
* <p>
* The user authorities are obtained from the data contained in the {@code memberOf}
* attribute.
*
* <p>
* <h3>Active Directory Sub-Error Codes</h3>
*
* <p>
* 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;
* <li>773 - user must reset password</li>
* <li>775 - account locked</li>
* </ul>
*
* <p>
* 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<? extends GrantedAuthority> 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<GrantedAuthority> 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}))}
* </p>
* @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 {

View File

@ -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<? extends GrantedAuthority> 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<GrantedAuthority> authorities = new ArrayList<>(groups.length);
for (String group : groups) {
authorities.add(new SimpleGrantedAuthority(new DistinguishedName(group).removeLast().getValue()));
}
return authorities;
}
}