Polish LDAP Module

Issue gh-3834
This commit is contained in:
Josh Cummings 2024-06-21 12:23:50 -06:00
parent 55895f3b08
commit 78d2be9bd5
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
25 changed files with 97 additions and 321 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2021 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +16,6 @@
package org.springframework.security.ldap; package org.springframework.security.ldap;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -84,7 +83,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy() { setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy() {
@Override @Override
@SuppressWarnings("rawtypes") @SuppressWarnings("unchecked")
public void setupEnvironment(Hashtable env, String dn, String password) { public void setupEnvironment(Hashtable env, String dn, String password) {
super.setupEnvironment(env, dn, password); super.setupEnvironment(env, dn, password);
// Remove the pooling flag unless authenticating as the 'manager' user. // Remove the pooling flag unless authenticating as the 'manager' user.
@ -145,7 +144,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
StringBuilder providerUrl = new StringBuilder(); StringBuilder providerUrl = new StringBuilder();
for (String serverUrl : urls) { for (String serverUrl : urls) {
String trimmedUrl = serverUrl.trim(); String trimmedUrl = serverUrl.trim();
if ("".equals(trimmedUrl)) { if (trimmedUrl.isEmpty()) {
continue; continue;
} }
providerUrl.append(trimmedUrl); providerUrl.append(trimmedUrl);
@ -160,21 +159,11 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource {
} }
private static String encodeUrl(String url) { private static String encodeUrl(String url) {
try { return URLEncoder.encode(url, StandardCharsets.UTF_8);
return URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
} }
private String decodeUrl(String url) { private String decodeUrl(String url) {
try { return URLDecoder.decode(url, StandardCharsets.UTF_8);
return URLDecoder.decode(url, StandardCharsets.UTF_8.toString());
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2005-2010 the original author or authors. * Copyright 2005-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,8 +16,6 @@
package org.springframework.security.ldap; package org.springframework.security.ldap;
import org.springframework.ldap.BadLdapGrammarException;
/** /**
* Helper class to encode and decode ldap names and values. * Helper class to encode and decode ldap names and values.
* *
@ -31,26 +29,7 @@ import org.springframework.ldap.BadLdapGrammarException;
*/ */
final class LdapEncoder { final class LdapEncoder {
private static final int HEX = 16; private static final String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
private static String[] NAME_ESCAPE_TABLE = new String[96];
static {
// all below 0x20 (control chars)
for (char c = 0; c < ' '; c++) {
NAME_ESCAPE_TABLE[c] = "\\" + toTwoCharHex(c);
}
NAME_ESCAPE_TABLE['#'] = "\\#";
NAME_ESCAPE_TABLE[','] = "\\,";
NAME_ESCAPE_TABLE[';'] = "\\;";
NAME_ESCAPE_TABLE['='] = "\\=";
NAME_ESCAPE_TABLE['+'] = "\\+";
NAME_ESCAPE_TABLE['<'] = "\\<";
NAME_ESCAPE_TABLE['>'] = "\\>";
NAME_ESCAPE_TABLE['\"'] = "\\\"";
NAME_ESCAPE_TABLE['\\'] = "\\\\";
}
private static String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
static { static {
// fill with char itself // fill with char itself
@ -71,11 +50,6 @@ final class LdapEncoder {
private LdapEncoder() { private LdapEncoder() {
} }
protected static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase();
return (raw.length() > 1) ? raw : "0" + raw;
}
/** /**
* Escape a value for use in a filter. * Escape a value for use in a filter.
* @param value the value to escape. * @param value the value to escape.
@ -94,102 +68,4 @@ final class LdapEncoder {
return encodedValue.toString(); return encodedValue.toString();
} }
/**
* LDAP Encodes a value for use with a DN. Escapes for LDAP, not JNDI!
*
* <br/>
* Escapes:<br/>
* ' ' [space] - "\ " [if first or last] <br/>
* '#' [hash] - "\#" <br/>
* ',' [comma] - "\," <br/>
* ';' [semicolon] - "\;" <br/>
* '= [equals] - "\=" <br/>
* '+' [plus] - "\+" <br/>
* '&lt;' [less than] - "\&lt;" <br/>
* '&gt;' [greater than] - "\&gt;" <br/>
* '"' [double quote] - "\"" <br/>
* '\' [backslash] - "\\" <br/>
* @param value the value to escape.
* @return The escaped value.
*/
static String nameEncode(String value) {
if (value == null) {
return null;
}
StringBuilder encodedValue = new StringBuilder(value.length() * 2);
int length = value.length();
int last = length - 1;
for (int i = 0; i < length; i++) {
char c = value.charAt(i);
// space first or last
if (c == ' ' && (i == 0 || i == last)) {
encodedValue.append("\\ ");
continue;
}
// check in table for escapes
if (c < NAME_ESCAPE_TABLE.length) {
String esc = NAME_ESCAPE_TABLE[c];
if (esc != null) {
encodedValue.append(esc);
continue;
}
}
// default: add the char
encodedValue.append(c);
}
return encodedValue.toString();
}
/**
* Decodes a value. Converts escaped chars to ordinary chars.
* @param value Trimmed value, so no leading an trailing blanks, except an escaped
* space last.
* @return The decoded value as a string.
* @throws BadLdapGrammarException
*/
static String nameDecode(String value) throws BadLdapGrammarException {
if (value == null) {
return null;
}
StringBuilder decoded = new StringBuilder(value.length());
int i = 0;
while (i < value.length()) {
char currentChar = value.charAt(i);
if (currentChar == '\\') {
// Ending with a single backslash is not allowed
if (value.length() <= i + 1) {
throw new BadLdapGrammarException("Unexpected end of value " + "unterminated '\\'");
}
char nextChar = value.charAt(i + 1);
if (isNormalBackslashEscape(nextChar)) {
decoded.append(nextChar);
i += 2;
}
else {
if (value.length() <= i + 2) {
throw new BadLdapGrammarException(
"Unexpected end of value " + "expected special or hex, found '" + nextChar + "'");
}
// This should be a hex value
String hexString = "" + nextChar + value.charAt(i + 2);
decoded.append((char) Integer.parseInt(hexString, HEX));
i += 3;
}
}
else {
// This character wasn't escaped - just append it
decoded.append(currentChar);
i++;
}
}
return decoded.toString();
}
private static boolean isNormalBackslashEscape(char nextChar) {
return nextChar == ',' || nextChar == '=' || nextChar == '+' || nextChar == '<' || nextChar == '>'
|| nextChar == '#' || nextChar == ';' || nextChar == '\\' || nextChar == '\"' || nextChar == ' ';
}
} }

View File

@ -83,7 +83,7 @@ public final class LdapUtils {
*/ */
public static String getRelativeName(String fullDn, Context baseCtx) throws NamingException { public static String getRelativeName(String fullDn, Context baseCtx) throws NamingException {
String baseDn = baseCtx.getNameInNamespace(); String baseDn = baseCtx.getNameInNamespace();
if (baseDn.length() == 0) { if (baseDn.isEmpty()) {
return fullDn; return fullDn;
} }
LdapName base = LdapNameBuilder.newInstance(baseDn).build(); LdapName base = LdapNameBuilder.newInstance(baseDn).build();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2013 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,7 +18,7 @@ package org.springframework.security.ldap;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -40,7 +40,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.core.ContextExecutor;
import org.springframework.ldap.core.ContextMapper; import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextAdapter;
@ -98,7 +97,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
searchControls.setSearchScope(SearchControls.OBJECT_SCOPE); searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
Object[] params = new Object[] { value }; Object[] params = new Object[] { value };
NamingEnumeration<SearchResult> results = ctx.search(dn, comparisonFilter, params, searchControls); NamingEnumeration<SearchResult> results = ctx.search(dn, comparisonFilter, params, searchControls);
Boolean match = results.hasMore(); boolean match = results.hasMore();
LdapUtils.closeEnumeration(results); LdapUtils.closeEnumeration(results);
return match; return match;
}); });
@ -112,7 +111,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
* @return the object created by the mapper * @return the object created by the mapper
*/ */
public DirContextOperations retrieveEntry(final String dn, final String[] attributesToRetrieve) { public DirContextOperations retrieveEntry(final String dn, final String[] attributesToRetrieve) {
return (DirContextOperations) executeReadOnly((ContextExecutor) (ctx) -> { return executeReadOnly((ctx) -> {
Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve); Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
return new DirContextAdapter(attrs, LdapNameBuilder.newInstance(dn).build(), return new DirContextAdapter(attrs, LdapNameBuilder.newInstance(dn).build(),
LdapNameBuilder.newInstance(ctx.getNameInNamespace()).build()); LdapNameBuilder.newInstance(ctx.getNameInNamespace()).build());
@ -169,18 +168,19 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
String formattedFilter = MessageFormat.format(filter, encodedParams); String formattedFilter = MessageFormat.format(filter, encodedParams);
logger.trace(LogMessage.format("Using filter: %s", formattedFilter)); logger.trace(LogMessage.format("Using filter: %s", formattedFilter));
HashSet<Map<String, List<String>>> result = new HashSet<>(); HashSet<Map<String, List<String>>> result = new HashSet<>();
ContextMapper roleMapper = (ctx) -> { ContextMapper<?> roleMapper = (ctx) -> {
DirContextAdapter adapter = (DirContextAdapter) ctx; DirContextAdapter adapter = (DirContextAdapter) ctx;
Map<String, List<String>> record = new HashMap<>(); Map<String, List<String>> record = new HashMap<>();
if (ObjectUtils.isEmpty(attributeNames)) { if (ObjectUtils.isEmpty(attributeNames)) {
try { try {
for (NamingEnumeration enumeration = adapter.getAttributes().getAll(); enumeration.hasMore();) { for (NamingEnumeration<? extends Attribute> enumeration = adapter.getAttributes()
Attribute attr = (Attribute) enumeration.next(); .getAll(); enumeration.hasMore();) {
Attribute attr = enumeration.next();
extractStringAttributeValues(adapter, record, attr.getID()); extractStringAttributeValues(adapter, record, attr.getID());
} }
} }
catch (NamingException ex) { catch (NamingException ex) {
org.springframework.ldap.support.LdapUtils.convertLdapException(ex); throw org.springframework.ldap.support.LdapUtils.convertLdapException(ex);
} }
} }
else { else {
@ -188,7 +188,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
extractStringAttributeValues(adapter, record, attributeName); extractStringAttributeValues(adapter, record, attributeName);
} }
} }
record.put(DN_KEY, Arrays.asList(getAdapterDN(adapter))); record.put(DN_KEY, Collections.singletonList(getAdapterDN(adapter)));
result.add(record); result.add(record);
return null; return null;
}; };
@ -258,8 +258,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
* search returns more than one result. * search returns more than one result.
*/ */
public DirContextOperations searchForSingleEntry(String base, String filter, Object[] params) { public DirContextOperations searchForSingleEntry(String base, String filter, Object[] params) {
return (DirContextOperations) executeReadOnly((ContextExecutor) (ctx) -> searchForSingleEntryInternal(ctx, return executeReadOnly((ctx) -> searchForSingleEntryInternal(ctx, this.searchControls, base, filter, params));
this.searchControls, base, filter, params));
} }
/** /**
@ -296,8 +295,9 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
/** /**
* We need to make sure the search controls has the return object flag set to true, in * We need to make sure the search controls has the return object flag set to true, in
* order for the search to return DirContextAdapter instances. * order for the search to return DirContextAdapter instances.
* @param originalControls * @param originalControls the {@link SearchControls} that might have the return
* @return * object flag set to true
* @return a {@link SearchControls} that does have the return object flag set to true
*/ */
private static SearchControls buildControls(SearchControls originalControls) { private static SearchControls buildControls(SearchControls originalControls) {
return new SearchControls(originalControls.getSearchScope(), originalControls.getCountLimit(), return new SearchControls(originalControls.getSearchScope(), originalControls.getCountLimit(),

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware; import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.lang.NonNull;
import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
@ -117,14 +118,15 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati
* obtained from the UserDetails object created by the configured * obtained from the UserDetails object created by the configured
* {@code UserDetailsContextMapper}. Often it will not be possible to read the * {@code UserDetailsContextMapper}. Often it will not be possible to read the
* password from the directory, so defaults to true. * password from the directory, so defaults to true.
* @param useAuthenticationRequestCredentials * @param useAuthenticationRequestCredentials whether to use the credentials in the
* authentication request
*/ */
public void setUseAuthenticationRequestCredentials(boolean useAuthenticationRequestCredentials) { public void setUseAuthenticationRequestCredentials(boolean useAuthenticationRequestCredentials) {
this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials; this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials;
} }
@Override @Override
public void setMessageSource(MessageSource messageSource) { public void setMessageSource(@NonNull MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource); this.messages = new MessageSourceAccessor(messageSource);
} }

View File

@ -25,6 +25,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware; import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.lang.NonNull;
import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.ContextSource;
import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.ldap.search.LdapUserSearch; import org.springframework.security.ldap.search.LdapUserSearch;
@ -37,6 +38,8 @@ import org.springframework.util.Assert;
*/ */
public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, InitializingBean, MessageSourceAware { public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, InitializingBean, MessageSourceAware {
private final Object mutex = new Object();
private final ContextSource contextSource; private final ContextSource contextSource;
/** /**
@ -59,7 +62,7 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
/** /**
* Create an initialized instance with the {@link ContextSource} provided. * Create an initialized instance with the {@link ContextSource} provided.
* @param contextSource * @param contextSource the {@link ContextSource} to use
*/ */
public AbstractLdapAuthenticator(ContextSource contextSource) { public AbstractLdapAuthenticator(ContextSource contextSource) {
Assert.notNull(contextSource, "contextSource must not be null."); Assert.notNull(contextSource, "contextSource must not be null.");
@ -93,7 +96,7 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
} }
List<String> userDns = new ArrayList<>(this.userDnFormat.length); List<String> userDns = new ArrayList<>(this.userDnFormat.length);
String[] args = new String[] { LdapEncoder.nameEncode(username) }; String[] args = new String[] { LdapEncoder.nameEncode(username) };
synchronized (this.userDnFormat) { synchronized (this.mutex) {
for (MessageFormat formatter : this.userDnFormat) { for (MessageFormat formatter : this.userDnFormat) {
userDns.add(formatter.format(args)); userDns.add(formatter.format(args));
} }
@ -106,14 +109,14 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
} }
@Override @Override
public void setMessageSource(MessageSource messageSource) { public void setMessageSource(@NonNull MessageSource messageSource) {
Assert.notNull(messageSource, "Message source must not be null"); Assert.notNull(messageSource, "Message source must not be null");
this.messages = new MessageSourceAccessor(messageSource); this.messages = new MessageSourceAccessor(messageSource);
} }
/** /**
* Sets the user attributes which will be retrieved from the directory. * Sets the user attributes which will be retrieved from the directory.
* @param userAttributes * @param userAttributes the set of user attributes to retrieve
*/ */
public void setUserAttributes(String[] userAttributes) { public void setUserAttributes(String[] userAttributes) {
Assert.notNull(userAttributes, "The userAttributes property cannot be set to null"); Assert.notNull(userAttributes, "The userAttributes property cannot be set to null");

View File

@ -33,7 +33,7 @@ public interface LdapAuthenticator {
/** /**
* Authenticates as a user and obtains additional user information from the directory. * Authenticates as a user and obtains additional user information from the directory.
* @param authentication * @param authentication the authentication request
* @return the details of the successfully authenticated user. * @return the details of the successfully authenticated user.
*/ */
DirContextOperations authenticate(Authentication authentication); DirContextOperations authenticate(Authentication authentication);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2005-2010 the original author or authors. * Copyright 2005-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,8 +16,6 @@
package org.springframework.security.ldap.authentication; package org.springframework.security.ldap.authentication;
import org.springframework.ldap.BadLdapGrammarException;
/** /**
* Helper class to encode and decode ldap names and values. * Helper class to encode and decode ldap names and values.
* *
@ -31,9 +29,7 @@ import org.springframework.ldap.BadLdapGrammarException;
*/ */
final class LdapEncoder { final class LdapEncoder {
private static final int HEX = 16; private static final String[] NAME_ESCAPE_TABLE = new String[96];
private static String[] NAME_ESCAPE_TABLE = new String[96];
static { static {
// all below 0x20 (control chars) // all below 0x20 (control chars)
for (char c = 0; c < ' '; c++) { for (char c = 0; c < ' '; c++) {
@ -50,54 +46,19 @@ final class LdapEncoder {
NAME_ESCAPE_TABLE['\\'] = "\\\\"; NAME_ESCAPE_TABLE['\\'] = "\\\\";
} }
private static String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
static {
// fill with char itself
for (char c = 0; c < FILTER_ESCAPE_TABLE.length; c++) {
FILTER_ESCAPE_TABLE[c] = String.valueOf(c);
}
// escapes (RFC2254)
FILTER_ESCAPE_TABLE['*'] = "\\2a";
FILTER_ESCAPE_TABLE['('] = "\\28";
FILTER_ESCAPE_TABLE[')'] = "\\29";
FILTER_ESCAPE_TABLE['\\'] = "\\5c";
FILTER_ESCAPE_TABLE[0] = "\\00";
}
/** /**
* All static methods - not to be instantiated. * All static methods - not to be instantiated.
*/ */
private LdapEncoder() { private LdapEncoder() {
} }
protected static String toTwoCharHex(char c) { static String toTwoCharHex(char c) {
String raw = Integer.toHexString(c).toUpperCase(); String raw = Integer.toHexString(c).toUpperCase();
return (raw.length() > 1) ? raw : "0" + raw; return (raw.length() > 1) ? raw : "0" + raw;
} }
/** /**
* Escape a value for use in a filter. * LDAP Encodes a value for use with a DN. Escapes for LDAP, not JNDI! <br/>
* @param value the value to escape.
* @return a properly escaped representation of the supplied value.
*/
static String filterEncode(String value) {
if (value == null) {
return null;
}
StringBuilder encodedValue = new StringBuilder(value.length() * 2);
int length = value.length();
for (int i = 0; i < length; i++) {
char ch = value.charAt(i);
encodedValue.append((ch < FILTER_ESCAPE_TABLE.length) ? FILTER_ESCAPE_TABLE[ch] : ch);
}
return encodedValue.toString();
}
/**
* LDAP Encodes a value for use with a DN. Escapes for LDAP, not JNDI!
*
* <br/>
* Escapes:<br/> * Escapes:<br/>
* ' ' [space] - "\ " [if first or last] <br/> * ' ' [space] - "\ " [if first or last] <br/>
* '#' [hash] - "\#" <br/> * '#' [hash] - "\#" <br/>
@ -140,56 +101,4 @@ final class LdapEncoder {
return encodedValue.toString(); return encodedValue.toString();
} }
/**
* Decodes a value. Converts escaped chars to ordinary chars.
* @param value Trimmed value, so no leading an trailing blanks, except an escaped
* space last.
* @return The decoded value as a string.
* @throws BadLdapGrammarException
*/
static String nameDecode(String value) throws BadLdapGrammarException {
if (value == null) {
return null;
}
StringBuilder decoded = new StringBuilder(value.length());
int i = 0;
while (i < value.length()) {
char currentChar = value.charAt(i);
if (currentChar == '\\') {
// Ending with a single backslash is not allowed
if (value.length() <= i + 1) {
throw new BadLdapGrammarException("Unexpected end of value " + "unterminated '\\'");
}
char nextChar = value.charAt(i + 1);
if (isNormalBackslashEscape(nextChar)) {
decoded.append(nextChar);
i += 2;
}
else {
if (value.length() <= i + 2) {
throw new BadLdapGrammarException(
"Unexpected end of value " + "expected special or hex, found '" + nextChar + "'");
}
// This should be a hex value
String hexString = "" + nextChar + value.charAt(i + 2);
decoded.append((char) Integer.parseInt(hexString, HEX));
i += 3;
}
}
else {
// This character wasn't escaped - just append it
decoded.append(currentChar);
i++;
}
}
return decoded.toString();
}
private static boolean isNormalBackslashEscape(char nextChar) {
return nextChar == ',' || nextChar == '=' || nextChar == '+' || nextChar == '<' || nextChar == '>'
|| nextChar == '#' || nextChar == ';' || nextChar == '\\' || nextChar == '\"' || nextChar == ' ';
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -57,8 +57,7 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource
return ""; return "";
} }
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
if (principal instanceof LdapUserDetails) { if (principal instanceof LdapUserDetails details) {
LdapUserDetails details = (LdapUserDetails) principal;
return details.getDn(); return details.getDn();
} }
if (authentication instanceof AnonymousAuthenticationToken) { if (authentication instanceof AnonymousAuthenticationToken) {

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -40,7 +40,6 @@ import org.springframework.security.core.AuthenticationException;
* *
* @author Rob Winch * @author Rob Winch
*/ */
@SuppressWarnings("serial")
public final class ActiveDirectoryAuthenticationException extends AuthenticationException { public final class ActiveDirectoryAuthenticationException extends AuthenticationException {
private final String dataCode; private final String dataCode;

View File

@ -189,12 +189,11 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
private DirContext bindAsUser(String username, String password) { private DirContext bindAsUser(String username, String password) {
// TODO. add DNS lookup based on domain // TODO. add DNS lookup based on domain
final String bindUrl = this.url;
Hashtable<String, Object> env = new Hashtable<>(); Hashtable<String, Object> env = new Hashtable<>();
env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_AUTHENTICATION, "simple");
String bindPrincipal = createBindPrincipal(username); String bindPrincipal = createBindPrincipal(username);
env.put(Context.SECURITY_PRINCIPAL, bindPrincipal); env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
env.put(Context.PROVIDER_URL, bindUrl); env.put(Context.PROVIDER_URL, this.url);
env.put(Context.SECURITY_CREDENTIALS, password); env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName()); env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName());

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015-2021 the original author or authors. * Copyright 2015-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,7 +30,7 @@ import org.springframework.security.ldap.userdetails.InetOrgPerson;
* @see LdapJackson2Module * @see LdapJackson2Module
* @see SecurityJackson2Modules * @see SecurityJackson2Modules
*/ */
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE) isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015-2021 the original author or authors. * Copyright 2015-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,7 +35,7 @@ import org.springframework.security.ldap.userdetails.LdapAuthority;
* @see LdapJackson2Module * @see LdapJackson2Module
* @see SecurityJackson2Modules * @see SecurityJackson2Modules
*/ */
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
abstract class LdapAuthorityMixin { abstract class LdapAuthorityMixin {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015-2021 the original author or authors. * Copyright 2015-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,6 +30,7 @@ import org.springframework.security.ldap.userdetails.Person;
* {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin}, * {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin},
* {@link InetOrgPersonMixin}. * {@link InetOrgPersonMixin}.
* *
* <p>
* If not already enabled, default typing will be automatically enabled as type info is * If not already enabled, default typing will be automatically enabled as type info is
* required to properly serialize/deserialize objects. In order to use this module just * required to properly serialize/deserialize objects. In order to use this module just
* add it to your {@code ObjectMapper} configuration. * add it to your {@code ObjectMapper} configuration.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015-2021 the original author or authors. * Copyright 2015-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,7 +30,7 @@ import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
* @see LdapJackson2Module * @see LdapJackson2Module
* @see SecurityJackson2Modules * @see SecurityJackson2Modules
*/ */
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE) isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2015-2021 the original author or authors. * Copyright 2015-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,7 +30,7 @@ import org.springframework.security.ldap.userdetails.Person;
* @see LdapJackson2Module * @see LdapJackson2Module
* @see SecurityJackson2Modules * @see SecurityJackson2Modules
*/ */
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE) isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -77,7 +77,7 @@ public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityConte
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
protected Hashtable getAuthenticatedEnv(String principal, String credentials) { protected Hashtable getAuthenticatedEnv(String principal, String credentials) {
Hashtable env = super.getAuthenticatedEnv(principal, credentials); Hashtable<String, Object> env = super.getAuthenticatedEnv(principal, credentials);
env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName()); env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName());
return env; return env;
} }

View File

@ -220,7 +220,7 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl {
} }
} }
class SpecificTagDecoder extends BERTagDecoder { static class SpecificTagDecoder extends BERTagDecoder {
/** Allows us to remember which of the two options we're decoding */ /** Allows us to remember which of the two options we're decoding */
private Boolean inChoice = null; private Boolean inChoice = null;

View File

@ -52,7 +52,7 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch {
/** /**
* Context name to search in, relative to the base of the configured ContextSource. * Context name to search in, relative to the base of the configured ContextSource.
*/ */
private String searchBase = ""; private final String searchBase;
/** /**
* The filter expression used in the user search. This is an LDAP search filter (as * The filter expression used in the user search. This is an LDAP search filter (as
@ -78,9 +78,9 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch {
this.contextSource = contextSource; this.contextSource = contextSource;
this.searchBase = searchBase; this.searchBase = searchBase;
setSearchSubtree(true); setSearchSubtree(true);
if (searchBase.length() == 0) { if (searchBase.isEmpty()) {
logger.info(LogMessage.format("Searches will be performed from the root %s since SearchBase not set", logger.info(LogMessage.format("Searches will be performed from the root %s since SearchBase not set",
contextSource.getBaseLdapPath())); contextSource.getBaseLdapName()));
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -33,6 +33,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.context.Lifecycle; import org.springframework.context.Lifecycle;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -43,7 +44,7 @@ public class UnboundIdContainer
private InMemoryDirectoryServer directoryServer; private InMemoryDirectoryServer directoryServer;
private String defaultPartitionSuffix; private final String defaultPartitionSuffix;
private int port = 53389; private int port = 53389;
@ -51,7 +52,7 @@ public class UnboundIdContainer
private boolean running; private boolean running;
private String ldif; private final String ldif;
public UnboundIdContainer(String defaultPartitionSuffix, String ldif) { public UnboundIdContainer(String defaultPartitionSuffix, String ldif) {
this.defaultPartitionSuffix = defaultPartitionSuffix; this.defaultPartitionSuffix = defaultPartitionSuffix;
@ -79,7 +80,7 @@ public class UnboundIdContainer
} }
@Override @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext; this.context = applicationContext;
} }

View File

@ -129,7 +129,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
/** /**
* The base DN from which the search for group membership should be performed * The base DN from which the search for group membership should be performed
*/ */
private String groupSearchBase; private final String groupSearchBase;
/** /**
* The pattern to be used for the user search. {0} is the user's DN * The pattern to be used for the user search. {0} is the user's DN
@ -166,7 +166,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
if (groupSearchBase == null) { if (groupSearchBase == null) {
logger.info("Will not perform group search since groupSearchBase is null."); logger.info("Will not perform group search since groupSearchBase is null.");
} }
else if (groupSearchBase.length() == 0) { else if (groupSearchBase.isEmpty()) {
logger.info("Will perform group search from the context source base since groupSearchBase is empty."); logger.info("Will perform group search from the context source base since groupSearchBase is empty.");
} }
this.authorityMapper = (record) -> { this.authorityMapper = (record) -> {
@ -365,16 +365,6 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
return this.convertToUpperCase; return this.convertToUpperCase;
} }
/**
* Returns the default role Method available so that classes extending this can
* override
* @return the default role used
* @see #setDefaultRole(String)
*/
private GrantedAuthority getDefaultRole() {
return this.defaultRole;
}
/** /**
* Returns the search controls Method available so that classes extending this can * Returns the search controls Method available so that classes extending this can
* override the search controls used * override the search controls used

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -31,16 +31,16 @@ import org.springframework.util.Assert;
*/ */
public class LdapAuthority implements GrantedAuthority { public class LdapAuthority implements GrantedAuthority {
private String dn; private final String dn;
private String role; private final String role;
private Map<String, List<String>> attributes; private final Map<String, List<String>> attributes;
/** /**
* Constructs an LdapAuthority that has a role and a DN but no other attributes * Constructs an LdapAuthority that has a role and a DN but no other attributes
* @param role * @param role the principal's role
* @param dn * @param dn the distinguished name
*/ */
public LdapAuthority(String role, String dn) { public LdapAuthority(String role, String dn) {
this(role, dn, null); this(role, dn, null);
@ -48,9 +48,9 @@ public class LdapAuthority implements GrantedAuthority {
/** /**
* Constructs an LdapAuthority with the given role, DN and other LDAP attributes * Constructs an LdapAuthority with the given role, DN and other LDAP attributes
* @param role * @param role the principal's role
* @param dn * @param dn the distinguished name
* @param attributes * @param attributes additional LDAP attributes
*/ */
public LdapAuthority(String role, String dn, Map<String, List<String>> attributes) { public LdapAuthority(String role, String dn, Map<String, List<String>> attributes) {
Assert.notNull(role, "role can not be null"); Assert.notNull(role, "role can not be null");
@ -70,7 +70,7 @@ public class LdapAuthority implements GrantedAuthority {
/** /**
* Returns the DN for this LDAP authority * Returns the DN for this LDAP authority
* @return * @return the distinguished name
*/ */
public String getDn() { public String getDn() {
return this.dn; return this.dn;
@ -91,7 +91,7 @@ public class LdapAuthority implements GrantedAuthority {
/** /**
* Returns the first attribute value for a specified attribute * Returns the first attribute value for a specified attribute
* @param name * @param name the attribute name
* @return the first attribute value for a specified attribute, may be null * @return the first attribute value for a specified attribute, may be null
*/ */
public String getFirstAttributeValue(String name) { public String getFirstAttributeValue(String name) {

View File

@ -44,7 +44,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.AttributesMapperCallbackHandler; import org.springframework.ldap.core.AttributesMapperCallbackHandler;
import org.springframework.ldap.core.ContextExecutor;
import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.DistinguishedName;
@ -121,7 +120,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
private final LdapTemplate template; private final LdapTemplate template;
/** Default context mapper used to create a set of roles from a list of attributes */ /** Default context mapper used to create a set of roles from a list of attributes */
private AttributesMapper roleMapper = (attributes) -> { private AttributesMapper<GrantedAuthority> roleMapper = (attributes) -> {
Attribute roleAttr = attributes.get(this.groupRoleAttributeName); Attribute roleAttr = attributes.get(this.groupRoleAttributeName);
NamingEnumeration<?> ne = roleAttr.getAll(); NamingEnumeration<?> ne = roleAttr.getAll();
Object group = ne.next(); Object group = ne.next();
@ -147,7 +146,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
} }
private DirContextAdapter loadUserAsContext(final LdapName dn, final String username) { private DirContextAdapter loadUserAsContext(final LdapName dn, final String username) {
return (DirContextAdapter) this.template.executeReadOnly((ContextExecutor) (ctx) -> { return this.template.executeReadOnly((ctx) -> {
try { try {
Attributes attrs = ctx.getAttributes(dn, this.attributesToRetrieve); Attributes attrs = ctx.getAttributes(dn, this.attributesToRetrieve);
return new DirContextAdapter(attrs, LdapUtils.getFullDn(dn, ctx)); return new DirContextAdapter(attrs, LdapUtils.getFullDn(dn, ctx));
@ -162,6 +161,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
* Changes the password for the current user. The username is obtained from the * Changes the password for the current user. The username is obtained from the
* security context. * security context.
* *
* <p>
* There are two supported strategies for modifying the user's password depending on * There are two supported strategies for modifying the user's password depending on
* the capabilities of the corresponding LDAP server. * the capabilities of the corresponding LDAP server.
* *
@ -170,6 +170,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
* <a target="_blank" href="https://tools.ietf.org/html/rfc3062"> LDAP Password Modify * <a target="_blank" href="https://tools.ietf.org/html/rfc3062"> LDAP Password Modify
* Extended Operation </a>. * Extended Operation </a>.
* *
* <p>
* See {@link LdapUserDetailsManager#setUsePasswordModifyExtensionOperation(boolean)} * See {@link LdapUserDetailsManager#setUsePasswordModifyExtensionOperation(boolean)}
* for details. * for details.
* </p> * </p>
@ -205,7 +206,6 @@ public class LdapUserDetailsManager implements UserDetailsManager {
* @param username the user whose roles are required. * @param username the user whose roles are required.
* @return the granted authorities returned by the group search * @return the granted authorities returned by the group search
*/ */
@SuppressWarnings("unchecked")
List<GrantedAuthority> getUserAuthorities(final LdapName dn, final String username) { List<GrantedAuthority> getUserAuthorities(final LdapName dn, final String username) {
SearchExecutor se = (ctx) -> { SearchExecutor se = (ctx) -> {
LdapName fullDn = LdapUtils.getFullDn(dn, ctx); LdapName fullDn = LdapUtils.getFullDn(dn, ctx);
@ -214,7 +214,8 @@ public class LdapUserDetailsManager implements UserDetailsManager {
return ctx.search(this.groupSearchBase, this.groupSearchFilter, return ctx.search(this.groupSearchBase, this.groupSearchFilter,
new String[] { fullDn.toString(), username }, ctrls); new String[] { fullDn.toString(), username }, ctrls);
}; };
AttributesMapperCallbackHandler roleCollector = new AttributesMapperCallbackHandler(this.roleMapper); AttributesMapperCallbackHandler<GrantedAuthority> roleCollector = new AttributesMapperCallbackHandler<>(
this.roleMapper);
this.template.search(se, roleCollector); this.template.search(se, roleCollector);
return roleCollector.getList(); return roleCollector.getList();
} }
@ -229,7 +230,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
// Check for any existing authorities which might be set for this // Check for any existing authorities which might be set for this
// DN and remove them // DN and remove them
List<GrantedAuthority> authorities = getUserAuthorities(dn, user.getUsername()); List<GrantedAuthority> authorities = getUserAuthorities(dn, user.getUsername());
if (authorities.size() > 0) { if (!authorities.isEmpty()) {
removeAuthorities(dn, authorities); removeAuthorities(dn, authorities);
} }
addAuthorities(dn, user.getAuthorities()); addAuthorities(dn, user.getAuthorities());
@ -322,7 +323,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
private void modifyAuthorities(final LdapName userDn, final Collection<? extends GrantedAuthority> authorities, private void modifyAuthorities(final LdapName userDn, final Collection<? extends GrantedAuthority> authorities,
final int modType) { final int modType) {
this.template.executeReadWrite((ContextExecutor) (ctx) -> { this.template.executeReadWrite((ctx) -> {
for (GrantedAuthority authority : authorities) { for (GrantedAuthority authority : authorities) {
String group = convertAuthorityToGroup(authority); String group = convertAuthorityToGroup(authority);
LdapName fullDn = LdapUtils.getFullDn(userDn, ctx); LdapName fullDn = LdapUtils.getFullDn(userDn, ctx);
@ -389,20 +390,26 @@ public class LdapUserDetailsManager implements UserDetailsManager {
/** /**
* Sets the method by which a user's password gets modified. * Sets the method by which a user's password gets modified.
* *
* <p>
* If set to {@code true}, then {@link LdapUserDetailsManager#changePassword} will * If set to {@code true}, then {@link LdapUserDetailsManager#changePassword} will
* modify the user's password by way of the * modify the user's password by way of the
* <a target="_blank" href="https://tools.ietf.org/html/rfc3062">Password Modify * <a target="_blank" href="https://tools.ietf.org/html/rfc3062">Password Modify
* Extension Operation</a>. * Extension Operation</a>.
* *
* <p>
* If set to {@code false}, then {@link LdapUserDetailsManager#changePassword} will * If set to {@code false}, then {@link LdapUserDetailsManager#changePassword} will
* modify the user's password by directly modifying attributes on the corresponding * modify the user's password by directly modifying attributes on the corresponding
* entry. * entry.
* *
* <p>
* Before using this setting, ensure that the corresponding LDAP server supports this * Before using this setting, ensure that the corresponding LDAP server supports this
* extended operation. * extended operation.
* *
* <p>
* By default, {@code usePasswordModifyExtensionOperation} is false. * By default, {@code usePasswordModifyExtensionOperation} is false.
* @param usePasswordModifyExtensionOperation * @param usePasswordModifyExtensionOperation whether to use the
* <a target="_blank" href="https://tools.ietf.org/html/rfc3062">Password Modify
* Extension Operation</a> to modify the password
* @since 4.2.9 * @since 4.2.9
*/ */
public void setUsePasswordModifyExtensionOperation(boolean usePasswordModifyExtensionOperation) { public void setUsePasswordModifyExtensionOperation(boolean usePasswordModifyExtensionOperation) {
@ -473,6 +480,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
* <a target="_blank" href="https://tools.ietf.org/html/rfc3062"> LDAP Password Modify * <a target="_blank" href="https://tools.ietf.org/html/rfc3062"> LDAP Password Modify
* Extended Operation </a> client request. * Extended Operation </a> client request.
* *
* <p>
* Can be directed at any LDAP server that supports the Password Modify Extended * Can be directed at any LDAP server that supports the Password Modify Extended
* Operation. * Operation.
* *

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2014 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -176,7 +176,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
if (getAttributeNames() == null) { if (getAttributeNames() == null) {
setAttributeNames(new HashSet<>()); setAttributeNames(new HashSet<>());
} }
if (StringUtils.hasText(getGroupRoleAttribute()) && !getAttributeNames().contains(getGroupRoleAttribute())) { if (StringUtils.hasText(getGroupRoleAttribute())) {
getAttributeNames().add(getGroupRoleAttribute()); getAttributeNames().add(getGroupRoleAttribute());
} }
Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues( Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues(
@ -200,7 +200,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
// this prevents a forever loop for a misconfigured ldap directory // this prevents a forever loop for a misconfigured ldap directory
circular = circular | (!authorities.add(new LdapAuthority(role, dn, record))); circular = circular | (!authorities.add(new LdapAuthority(role, dn, record)));
} }
String roleName = (roles.size() > 0) ? roles.iterator().next() : dn; String roleName = (!roles.isEmpty()) ? roles.iterator().next() : dn;
if (!circular) { if (!circular) {
performNestedSearch(dn, roleName, authorities, (depth - 1)); performNestedSearch(dn, roleName, authorities, (depth - 1));
} }

View File

@ -39,7 +39,7 @@ public interface UserDetailsContextMapper {
* Creates a fully populated UserDetails object for use by the security framework. * Creates a fully populated UserDetails object for use by the security framework.
* @param ctx the context object which contains the user information. * @param ctx the context object which contains the user information.
* @param username the user's supplied login name. * @param username the user's supplied login name.
* @param authorities * @param authorities the authorities to add to the {@code UserDetails} instance
* @return the user object. * @return the user object.
*/ */
UserDetails mapUserFromContext(DirContextOperations ctx, String username, UserDetails mapUserFromContext(DirContextOperations ctx, String username,