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

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");
* you may not use this file except in compliance with the License.
@ -16,8 +16,6 @@
package org.springframework.security.ldap;
import org.springframework.ldap.BadLdapGrammarException;
/**
* Helper class to encode and decode ldap names and values.
*
@ -31,26 +29,7 @@ import org.springframework.ldap.BadLdapGrammarException;
*/
final class LdapEncoder {
private static final int HEX = 16;
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];
private static final String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
static {
// fill with char itself
@ -71,11 +50,6 @@ final class 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.
* @param value the value to escape.
@ -94,102 +68,4 @@ final class LdapEncoder {
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 {
String baseDn = baseCtx.getNameInNamespace();
if (baseDn.length() == 0) {
if (baseDn.isEmpty()) {
return fullDn;
}
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");
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -40,7 +40,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.core.ContextExecutor;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextAdapter;
@ -98,7 +97,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
Object[] params = new Object[] { value };
NamingEnumeration<SearchResult> results = ctx.search(dn, comparisonFilter, params, searchControls);
Boolean match = results.hasMore();
boolean match = results.hasMore();
LdapUtils.closeEnumeration(results);
return match;
});
@ -112,7 +111,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
* @return the object created by the mapper
*/
public DirContextOperations retrieveEntry(final String dn, final String[] attributesToRetrieve) {
return (DirContextOperations) executeReadOnly((ContextExecutor) (ctx) -> {
return executeReadOnly((ctx) -> {
Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
return new DirContextAdapter(attrs, LdapNameBuilder.newInstance(dn).build(),
LdapNameBuilder.newInstance(ctx.getNameInNamespace()).build());
@ -169,18 +168,19 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
String formattedFilter = MessageFormat.format(filter, encodedParams);
logger.trace(LogMessage.format("Using filter: %s", formattedFilter));
HashSet<Map<String, List<String>>> result = new HashSet<>();
ContextMapper roleMapper = (ctx) -> {
ContextMapper<?> roleMapper = (ctx) -> {
DirContextAdapter adapter = (DirContextAdapter) ctx;
Map<String, List<String>> record = new HashMap<>();
if (ObjectUtils.isEmpty(attributeNames)) {
try {
for (NamingEnumeration enumeration = adapter.getAttributes().getAll(); enumeration.hasMore();) {
Attribute attr = (Attribute) enumeration.next();
for (NamingEnumeration<? extends Attribute> enumeration = adapter.getAttributes()
.getAll(); enumeration.hasMore();) {
Attribute attr = enumeration.next();
extractStringAttributeValues(adapter, record, attr.getID());
}
}
catch (NamingException ex) {
org.springframework.ldap.support.LdapUtils.convertLdapException(ex);
throw org.springframework.ldap.support.LdapUtils.convertLdapException(ex);
}
}
else {
@ -188,7 +188,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
extractStringAttributeValues(adapter, record, attributeName);
}
}
record.put(DN_KEY, Arrays.asList(getAdapterDN(adapter)));
record.put(DN_KEY, Collections.singletonList(getAdapterDN(adapter)));
result.add(record);
return null;
};
@ -258,8 +258,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
* search returns more than one result.
*/
public DirContextOperations searchForSingleEntry(String base, String filter, Object[] params) {
return (DirContextOperations) executeReadOnly((ContextExecutor) (ctx) -> searchForSingleEntryInternal(ctx,
this.searchControls, base, filter, params));
return executeReadOnly((ctx) -> searchForSingleEntryInternal(ctx, 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
* order for the search to return DirContextAdapter instances.
* @param originalControls
* @return
* @param originalControls the {@link SearchControls} that might have the 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) {
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");
* 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.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.lang.NonNull;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider;
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
* {@code UserDetailsContextMapper}. Often it will not be possible to read the
* 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) {
this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials;
}
@Override
public void setMessageSource(MessageSource messageSource) {
public void setMessageSource(@NonNull MessageSource 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.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.lang.NonNull;
import org.springframework.ldap.core.ContextSource;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.ldap.search.LdapUserSearch;
@ -37,6 +38,8 @@ import org.springframework.util.Assert;
*/
public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, InitializingBean, MessageSourceAware {
private final Object mutex = new Object();
private final ContextSource contextSource;
/**
@ -59,7 +62,7 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
/**
* Create an initialized instance with the {@link ContextSource} provided.
* @param contextSource
* @param contextSource the {@link ContextSource} to use
*/
public AbstractLdapAuthenticator(ContextSource contextSource) {
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);
String[] args = new String[] { LdapEncoder.nameEncode(username) };
synchronized (this.userDnFormat) {
synchronized (this.mutex) {
for (MessageFormat formatter : this.userDnFormat) {
userDns.add(formatter.format(args));
}
@ -106,14 +109,14 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
}
@Override
public void setMessageSource(MessageSource messageSource) {
public void setMessageSource(@NonNull MessageSource messageSource) {
Assert.notNull(messageSource, "Message source must not be null");
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* 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) {
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.
* @param authentication
* @param authentication the authentication request
* @return the details of the successfully authenticated user.
*/
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");
* you may not use this file except in compliance with the License.
@ -16,8 +16,6 @@
package org.springframework.security.ldap.authentication;
import org.springframework.ldap.BadLdapGrammarException;
/**
* Helper class to encode and decode ldap names and values.
*
@ -31,9 +29,7 @@ import org.springframework.ldap.BadLdapGrammarException;
*/
final class LdapEncoder {
private static final int HEX = 16;
private static String[] NAME_ESCAPE_TABLE = new String[96];
private static final String[] NAME_ESCAPE_TABLE = new String[96];
static {
// all below 0x20 (control chars)
for (char c = 0; c < ' '; c++) {
@ -50,54 +46,19 @@ final class LdapEncoder {
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.
*/
private LdapEncoder() {
}
protected static String toTwoCharHex(char c) {
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.
* @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/>
* 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/>
@ -140,56 +101,4 @@ final class LdapEncoder {
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");
* you may not use this file except in compliance with the License.
@ -57,8 +57,7 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource
return "";
}
Object principal = authentication.getPrincipal();
if (principal instanceof LdapUserDetails) {
LdapUserDetails details = (LdapUserDetails) principal;
if (principal instanceof LdapUserDetails details) {
return details.getDn();
}
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");
* 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
*/
@SuppressWarnings("serial")
public final class ActiveDirectoryAuthenticationException extends AuthenticationException {
private final String dataCode;

View File

@ -189,12 +189,11 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
private DirContext bindAsUser(String username, String password) {
// TODO. add DNS lookup based on domain
final String bindUrl = this.url;
Hashtable<String, Object> env = new Hashtable<>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
String bindPrincipal = createBindPrincipal(username);
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.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
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");
* 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 SecurityJackson2Modules
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@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");
* 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 SecurityJackson2Modules
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
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");
* 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 InetOrgPersonMixin}.
*
* <p>
* 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
* 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");
* 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 SecurityJackson2Modules
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@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");
* 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 SecurityJackson2Modules
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@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");
* you may not use this file except in compliance with the License.
@ -77,7 +77,7 @@ public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityConte
@Override
@SuppressWarnings("unchecked")
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());
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 */
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.
*/
private String searchBase = "";
private final String searchBase;
/**
* 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.searchBase = searchBase;
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",
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");
* 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.Lifecycle;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
/**
@ -43,7 +44,7 @@ public class UnboundIdContainer
private InMemoryDirectoryServer directoryServer;
private String defaultPartitionSuffix;
private final String defaultPartitionSuffix;
private int port = 53389;
@ -51,7 +52,7 @@ public class UnboundIdContainer
private boolean running;
private String ldif;
private final String ldif;
public UnboundIdContainer(String defaultPartitionSuffix, String ldif) {
this.defaultPartitionSuffix = defaultPartitionSuffix;
@ -79,7 +80,7 @@ public class UnboundIdContainer
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
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
*/
private String groupSearchBase;
private final String groupSearchBase;
/**
* 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) {
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.");
}
this.authorityMapper = (record) -> {
@ -365,16 +365,6 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
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
* 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");
* 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 {
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
* @param role
* @param dn
* @param role the principal's role
* @param dn the distinguished name
*/
public LdapAuthority(String role, String dn) {
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
* @param role
* @param dn
* @param attributes
* @param role the principal's role
* @param dn the distinguished name
* @param attributes additional LDAP attributes
*/
public LdapAuthority(String role, String dn, Map<String, List<String>> attributes) {
Assert.notNull(role, "role can not be null");
@ -70,7 +70,7 @@ public class LdapAuthority implements GrantedAuthority {
/**
* Returns the DN for this LDAP authority
* @return
* @return the distinguished name
*/
public String getDn() {
return this.dn;
@ -91,7 +91,7 @@ public class LdapAuthority implements GrantedAuthority {
/**
* 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
*/
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.ldap.core.AttributesMapper;
import org.springframework.ldap.core.AttributesMapperCallbackHandler;
import org.springframework.ldap.core.ContextExecutor;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DistinguishedName;
@ -121,7 +120,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
private final LdapTemplate template;
/** 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);
NamingEnumeration<?> ne = roleAttr.getAll();
Object group = ne.next();
@ -147,7 +146,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
}
private DirContextAdapter loadUserAsContext(final LdapName dn, final String username) {
return (DirContextAdapter) this.template.executeReadOnly((ContextExecutor) (ctx) -> {
return this.template.executeReadOnly((ctx) -> {
try {
Attributes attrs = ctx.getAttributes(dn, this.attributesToRetrieve);
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
* security context.
*
* <p>
* There are two supported strategies for modifying the user's password depending on
* 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
* Extended Operation </a>.
*
* <p>
* See {@link LdapUserDetailsManager#setUsePasswordModifyExtensionOperation(boolean)}
* for details.
* </p>
@ -205,7 +206,6 @@ public class LdapUserDetailsManager implements UserDetailsManager {
* @param username the user whose roles are required.
* @return the granted authorities returned by the group search
*/
@SuppressWarnings("unchecked")
List<GrantedAuthority> getUserAuthorities(final LdapName dn, final String username) {
SearchExecutor se = (ctx) -> {
LdapName fullDn = LdapUtils.getFullDn(dn, ctx);
@ -214,7 +214,8 @@ public class LdapUserDetailsManager implements UserDetailsManager {
return ctx.search(this.groupSearchBase, this.groupSearchFilter,
new String[] { fullDn.toString(), username }, ctrls);
};
AttributesMapperCallbackHandler roleCollector = new AttributesMapperCallbackHandler(this.roleMapper);
AttributesMapperCallbackHandler<GrantedAuthority> roleCollector = new AttributesMapperCallbackHandler<>(
this.roleMapper);
this.template.search(se, roleCollector);
return roleCollector.getList();
}
@ -229,7 +230,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
// Check for any existing authorities which might be set for this
// DN and remove them
List<GrantedAuthority> authorities = getUserAuthorities(dn, user.getUsername());
if (authorities.size() > 0) {
if (!authorities.isEmpty()) {
removeAuthorities(dn, authorities);
}
addAuthorities(dn, user.getAuthorities());
@ -322,7 +323,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
private void modifyAuthorities(final LdapName userDn, final Collection<? extends GrantedAuthority> authorities,
final int modType) {
this.template.executeReadWrite((ContextExecutor) (ctx) -> {
this.template.executeReadWrite((ctx) -> {
for (GrantedAuthority authority : authorities) {
String group = convertAuthorityToGroup(authority);
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.
*
* <p>
* If set to {@code true}, then {@link LdapUserDetailsManager#changePassword} will
* modify the user's password by way of the
* <a target="_blank" href="https://tools.ietf.org/html/rfc3062">Password Modify
* Extension Operation</a>.
*
* <p>
* If set to {@code false}, then {@link LdapUserDetailsManager#changePassword} will
* modify the user's password by directly modifying attributes on the corresponding
* entry.
*
* <p>
* Before using this setting, ensure that the corresponding LDAP server supports this
* extended operation.
*
* <p>
* 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
*/
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
* Extended Operation </a> client request.
*
* <p>
* Can be directed at any LDAP server that supports the Password Modify Extended
* 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");
* you may not use this file except in compliance with the License.
@ -176,7 +176,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
if (getAttributeNames() == null) {
setAttributeNames(new HashSet<>());
}
if (StringUtils.hasText(getGroupRoleAttribute()) && !getAttributeNames().contains(getGroupRoleAttribute())) {
if (StringUtils.hasText(getGroupRoleAttribute())) {
getAttributeNames().add(getGroupRoleAttribute());
}
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
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) {
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.
* @param ctx the context object which contains the user information.
* @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.
*/
UserDetails mapUserFromContext(DirContextOperations ctx, String username,