Skip to content

Commit

Permalink
Add ActiveDirectoryLdapAuthenticationProvider#setAuthoritiesPopulator
Browse files Browse the repository at this point in the history
Closes gh-4490
  • Loading branch information
Haarolean authored and jzheaux committed Feb 9, 2024
1 parent aafa4dd commit 98f0a21
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -39,7 +36,6 @@
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;
Expand All @@ -50,11 +46,10 @@
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;

Expand All @@ -72,9 +67,9 @@
* <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
Expand All @@ -90,13 +85,14 @@
* <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 {
Expand Down Expand Up @@ -135,27 +131,31 @@ 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) {
Assert.isTrue(StringUtils.hasText(url), "Url cannot be empty");
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
Expand All @@ -179,26 +179,10 @@ protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationTo
}
}

/**
* 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) {
Expand Down Expand Up @@ -332,14 +316,14 @@ private String searchRootFromPrincipal(String bindPrincipal) {
+ "' 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);
Expand Down Expand Up @@ -379,7 +363,6 @@ public void setConvertSubErrorCodesToExceptions(boolean convertSubErrorCodesToEx
* Defaults to: {@code (&(objectClass=user)(userPrincipalName={0}))}
* </p>
* @param searchFilter the filter string
*
* @since 3.2.6
*/
public void setSearchFilter(String searchFilter) {
Expand All @@ -397,6 +380,19 @@ public void setContextEnvironmentProperties(Map<String, Object> environment) {
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}

0 comments on commit 98f0a21

Please sign in to comment.