1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Add security-nullability to ldap

Closes gh-17818

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
Josh Cummings
2026-01-27 18:05:02 -07:00
parent a8b5c8fe02
commit c5632ccd83
42 changed files with 369 additions and 174 deletions
@@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -98,12 +99,14 @@ public class LdapBindAuthenticationManagerFactoryITests {
public void authenticationManagerFactoryWhenCustomUserDetailsContextMapperThenUsed() throws Exception {
CustomUserDetailsContextMapperConfig.CONTEXT_MAPPER = new UserDetailsContextMapper() {
@Override
@NullMarked
public UserDetails mapUserFromContext(DirContextOperations ctx, String username,
Collection<? extends GrantedAuthority> authorities) {
return User.withUsername("other").password("password").roles("USER").build();
}
@Override
@NullMarked
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
}
};
+1
View File
@@ -1,6 +1,7 @@
apply plugin: 'io.spring.convention.spring-module'
apply plugin: 'javadoc-warnings-error'
apply plugin: 'compile-warnings-error'
apply plugin: 'security-nullability'
dependencies {
management platform(project(":spring-security-dependencies"))
@@ -16,6 +16,8 @@
package org.springframework.security.ldap;
import org.jspecify.annotations.Nullable;
/**
* Helper class to encode and decode ldap names and values.
*
@@ -29,7 +31,7 @@ package org.springframework.security.ldap;
*/
final class LdapEncoder {
private static final String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
private static final @Nullable String[] FILTER_ESCAPE_TABLE = new String['\\' + 1];
static {
// fill with char itself
@@ -56,9 +58,6 @@ final class LdapEncoder {
* @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++) {
@@ -26,6 +26,7 @@ import javax.naming.ldap.LdapName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.support.LdapNameBuilder;
@@ -44,7 +45,7 @@ public final class LdapUtils {
private LdapUtils() {
}
public static void closeContext(Context ctx) {
public static void closeContext(@Nullable Context ctx) {
if (ctx instanceof DirContextAdapter) {
return;
}
@@ -58,7 +59,7 @@ public final class LdapUtils {
}
}
public static void closeEnumeration(NamingEnumeration ne) {
public static void closeEnumeration(@Nullable NamingEnumeration ne) {
try {
if (ne != null) {
ne.close();
@@ -37,6 +37,7 @@ import javax.naming.ldap.LdapName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
@@ -110,7 +111,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
* directory entry.
* @return the object created by the mapper
*/
public DirContextOperations retrieveEntry(final String dn, final String[] attributesToRetrieve) {
public DirContextOperations retrieveEntry(final String dn, final String @Nullable [] attributesToRetrieve) {
return executeReadOnly((ctx) -> {
Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
return new DirContextAdapter(attrs, LdapNameBuilder.newInstance(dn).build(),
@@ -153,13 +154,14 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
* @param base the DN to search in
* @param filter search filter to use
* @param params the parameters to substitute in the search filter
* @param attributeNames the attributes' values that are to be retrieved.
* @param attributeNames the attributes' values that are to be retrieved; a
* {@code null} array means retrieve all attributes
* @return the set of String values for each attribute found in all the matching
* entries. The attribute name is the key for each set of values. In addition each map
* contains the DN as a String with the key predefined key {@link #DN_KEY}.
*/
public Set<Map<String, List<String>>> searchForMultipleAttributeValues(String base, String filter, Object[] params,
String[] attributeNames) {
String @Nullable [] attributeNames) {
// Escape the params acording to RFC2254
Object[] encodedParams = new String[params.length];
for (int i = 0; i < params.length; i++) {
@@ -190,7 +192,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
}
record.put(DN_KEY, Collections.singletonList(getAdapterDN(adapter)));
result.add(record);
return null;
return void.class;
};
SearchControls ctls = new SearchControls();
ctls.setSearchScope(this.searchControls.getSearchScope());
@@ -16,6 +16,8 @@
package org.springframework.security.ldap.aot.hint;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -30,7 +32,7 @@ import org.springframework.aot.hint.TypeReference;
class LdapSecurityRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.reflection()
.registerType(TypeReference.of("com.sun.jndi.ldap.LdapCtxFactory"),
(builder) -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
@@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Package for AOT Hints
*/
@NullMarked
package org.springframework.security.ldap.aot.hint;
import org.jspecify.annotations.NullMarked;
@@ -21,7 +21,6 @@ import java.util.LinkedHashSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
@@ -83,7 +82,7 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati
Assert.notNull(password, "Null password was supplied in authentication token");
DirContextOperations userData = doAuthentication(userToken);
UserDetails user = this.userDetailsContextMapper.mapUserFromContext(userData, authentication.getName(),
loadUserAuthorities(userData, authentication.getName(), (String) authentication.getCredentials()));
loadUserAuthorities(userData, authentication.getName(), password));
return createSuccessfulAuthentication(userToken, user);
}
@@ -133,7 +132,7 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati
}
@Override
public void setMessageSource(@NonNull MessageSource messageSource) {
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
@@ -21,7 +21,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
@@ -47,7 +47,7 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
* Optional search object which can be used to locate a user when a simple DN match
* isn't sufficient
*/
private LdapUserSearch userSearch;
private @Nullable LdapUserSearch userSearch;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
@@ -55,11 +55,11 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
* The attributes which will be retrieved from the directory. Null means all
* attributes
*/
private String[] userAttributes = null;
private String @Nullable [] userAttributes = null;
// private String[] userDnPattern = null;
/** Stores the patterns which are used as potential DN matches */
private MessageFormat[] userDnFormat = null;
private MessageFormat @Nullable [] userDnFormat = null;
/**
* Create an initialized instance with the {@link ContextSource} provided.
@@ -80,7 +80,7 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
return this.contextSource;
}
public String[] getUserAttributes() {
public String @Nullable [] getUserAttributes() {
return this.userAttributes;
}
@@ -105,12 +105,12 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator, In
return userDns;
}
protected LdapUserSearch getUserSearch() {
protected @Nullable LdapUserSearch getUserSearch() {
return this.userSearch;
}
@Override
public void setMessageSource(@NonNull MessageSource messageSource) {
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "Message source must not be null");
this.messages = new MessageSourceAccessor(messageSource);
}
@@ -22,6 +22,7 @@ import javax.naming.directory.DirContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.ldap.NamingException;
@@ -98,11 +99,12 @@ public class BindAuthenticator extends AbstractLdapAuthenticator {
return user;
}
private DirContextOperations bindWithDn(String userDnStr, String username, String password) {
private @Nullable DirContextOperations bindWithDn(String userDnStr, String username, String password) {
return bindWithDn(userDnStr, username, password, null);
}
private DirContextOperations bindWithDn(String userDnStr, String username, String password, Attributes attrs) {
private @Nullable DirContextOperations bindWithDn(String userDnStr, String username, String password,
@Nullable Attributes attrs) {
BaseLdapPathContextSource ctxSource = (BaseLdapPathContextSource) getContextSource();
Name userDn = LdapUtils.newLdapName(userDnStr);
Name fullDn = LdapUtils.prepend(userDn, ctxSource.getBaseLdapName());
@@ -18,6 +18,8 @@ package org.springframework.security.ldap.authentication;
import java.util.Locale;
import org.jspecify.annotations.Nullable;
/**
* Helper class to encode and decode ldap names and values.
*
@@ -31,7 +33,7 @@ import java.util.Locale;
*/
final class LdapEncoder {
private static final String[] NAME_ESCAPE_TABLE = new String[96];
private static final @Nullable String[] NAME_ESCAPE_TABLE = new String[96];
static {
// all below 0x20 (control chars)
for (char c = 0; c < ' '; c++) {
@@ -76,9 +78,6 @@ final class LdapEncoder {
* @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;
@@ -18,6 +18,7 @@ package org.springframework.security.ldap.authentication;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.ldap.NameNotFoundException;
@@ -101,9 +102,6 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
if (user == null && getUserSearch() != null) {
logger.trace("Searching for user using " + getUserSearch());
user = getUserSearch().searchForUser(username);
if (user == null) {
logger.debug("Failed to find user using " + getUserSearch());
}
}
if (user == null) {
throw UsernameNotFoundException.fromUsername(username);
@@ -117,6 +115,7 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
this.passwordAttributeName, user.getDn()));
return user;
}
Assert.notNull(password, "LDAP password cannot be null");
if (isLdapPasswordCompare(user, ldapTemplate, password)) {
logger.debug(LogMessage.format("LDAP-matched password attribute '%s' for user '%s'",
this.passwordAttributeName, user.getDn()));
@@ -126,12 +125,12 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
this.messages.getMessage("PasswordComparisonAuthenticator.badCredentials", "Bad credentials"));
}
private boolean isPasswordAttrCompare(DirContextOperations user, String password) {
private boolean isPasswordAttrCompare(DirContextOperations user, @Nullable String password) {
String passwordAttrValue = getPassword(user);
return this.passwordEncoder.matches(password, passwordAttrValue);
}
private String getPassword(DirContextOperations user) {
private @Nullable String getPassword(DirContextOperations user) {
Object passwordAttrValue = user.getObjectAttribute(this.passwordAttributeName);
if (passwordAttrValue == null) {
return null;
@@ -78,7 +78,12 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource
log.debug("Returning empty String as Credentials since authentication is null");
return "";
}
return (String) authentication.getCredentials();
String password = (String) authentication.getCredentials();
if (password == null) {
log.debug("Returning empty String as Credentials since password is null");
return "";
}
return password;
}
/**
@@ -16,6 +16,8 @@
package org.springframework.security.ldap.authentication.ad;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.AuthenticationException;
/**
@@ -45,7 +47,7 @@ public final class ActiveDirectoryAuthenticationException extends Authentication
private final String dataCode;
ActiveDirectoryAuthenticationException(String dataCode, String message, Throwable cause) {
ActiveDirectoryAuthenticationException(String dataCode, @Nullable String message, Throwable cause) {
super(message, cause);
this.dataCode = dataCode;
}
@@ -33,6 +33,8 @@ import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.CommunicationException;
@@ -117,9 +119,9 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
private static final int ACCOUNT_LOCKED = 0x775;
private final String domain;
private final @Nullable String domain;
private final String rootDn;
private final @Nullable String rootDn;
private final String url;
@@ -163,6 +165,7 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
String username = auth.getName();
String password = (String) auth.getCredentials();
Assert.notNull(password, "password cannot be null");
DirContext ctx = null;
try {
ctx = bindAsUser(username, password);
@@ -236,7 +239,10 @@ public final class ActiveDirectoryLdapAuthenticationProvider extends AbstractLda
}
}
private int parseSubErrorCode(String message) {
private int parseSubErrorCode(@Nullable String message) {
if (message == null) {
return -1;
}
Matcher matcher = SUB_ERROR_CODE.matcher(message);
if (matcher.matches()) {
return Integer.parseInt(matcher.group(1), 16);
@@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Package for ActiveDirectory support
*/
@NullMarked
package org.springframework.security.ldap.authentication.ad;
import org.jspecify.annotations.NullMarked;
@@ -22,4 +22,7 @@
* an <tt>LdapAuthenticator</tt> instance and an <tt>LdapAuthoritiesPopulator</tt>. The
* latter is used to obtain the list of roles for the user.
*/
@NullMarked
package org.springframework.security.ldap.authentication;
import org.jspecify.annotations.NullMarked;
@@ -17,4 +17,7 @@
/**
* Jackson 3+ serialization support for LDAP.
*/
@NullMarked
package org.springframework.security.ldap.jackson;
import org.jspecify.annotations.NullMarked;
@@ -17,4 +17,7 @@
/**
* Jackson 2 serialization support for LDAP.
*/
@NullMarked
package org.springframework.security.ldap.jackson2;
import org.jspecify.annotations.NullMarked;
@@ -17,4 +17,7 @@
/**
* Spring Security's LDAP module.
*/
@NullMarked
package org.springframework.security.ldap;
import org.jspecify.annotations.NullMarked;
@@ -65,7 +65,7 @@ public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityConte
this.logger.debug(LogMessage.format("Failed to bind with %s", ctrl), ex);
}
LdapUtils.closeContext(ctx);
if (ctrl != null && ctrl.isLocked()) {
if (ctrl != null && ctrl.isLocked() && ctrl.getErrorStatus() != null) {
throw new PasswordPolicyException(ctrl.getErrorStatus());
}
throw LdapUtils.convertLdapException(ex);
@@ -20,6 +20,8 @@ import java.io.Serial;
import javax.naming.ldap.Control;
import org.jspecify.annotations.Nullable;
/**
*
* A Password Policy request control.
@@ -65,7 +67,7 @@ public class PasswordPolicyControl implements Control {
* @return always null
*/
@Override
public byte[] getEncodedValue() {
public byte @Nullable [] getEncodedValue() {
return null;
}
@@ -22,6 +22,7 @@ import javax.naming.ldap.LdapContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
/**
* Obtains the <tt>PasswordPolicyControl</tt> from a context for use by other classes.
@@ -36,7 +37,7 @@ public final class PasswordPolicyControlExtractor {
private PasswordPolicyControlExtractor() {
}
public static PasswordPolicyResponseControl extractControl(DirContext dirCtx) {
public static @Nullable PasswordPolicyResponseControl extractControl(DirContext dirCtx) {
LdapContext ctx = (LdapContext) dirCtx;
Control[] ctrls = null;
try {
@@ -19,6 +19,8 @@ package org.springframework.security.ldap.ppolicy;
import javax.naming.ldap.Control;
import javax.naming.ldap.ControlFactory;
import org.jspecify.annotations.Nullable;
/**
* Transforms a control object to a PasswordPolicyResponseControl object, if appropriate.
*
@@ -35,7 +37,7 @@ public class PasswordPolicyControlFactory extends ControlFactory {
* @return a response control of type PasswordPolicyResponseControl, or null
*/
@Override
public Control getControlInstance(Control ctl) {
public @Nullable Control getControlInstance(Control ctl) {
if (ctl.getID().equals(PasswordPolicyControl.OID)) {
return new PasswordPolicyResponseControl(ctl.getEncodedValue());
}
@@ -31,6 +31,7 @@ import netscape.ldap.ber.stream.BERTag;
import netscape.ldap.ber.stream.BERTagDecoder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.dao.DataRetrievalFailureException;
@@ -59,7 +60,7 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl {
private final byte[] encodedValue;
private PasswordPolicyErrorStatus errorStatus;
private @Nullable PasswordPolicyErrorStatus errorStatus;
private int graceLoginsRemaining = Integer.MAX_VALUE;
@@ -100,7 +101,7 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl {
return this.encodedValue;
}
public PasswordPolicyErrorStatus getErrorStatus() {
public @Nullable PasswordPolicyErrorStatus getErrorStatus() {
return this.errorStatus;
}
@@ -165,7 +166,7 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl {
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName()).append(" [");
if (hasError()) {
if (hasError() && this.errorStatus != null) {
sb.append("error=").append(this.errorStatus.getDefaultMessage()).append("; ");
}
if (this.graceLoginsRemaining != Integer.MAX_VALUE) {
@@ -227,7 +228,7 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl {
static class SpecificTagDecoder extends BERTagDecoder {
/** Allows us to remember which of the two options we're decoding */
private Boolean inChoice = null;
private @Nullable Boolean inChoice = null;
@Override
public BERElement getElement(BERTagDecoder decoder, int tag, InputStream stream, int[] bytesRead,
@@ -22,4 +22,7 @@
* This code will not work with servers such as Active Directory, which do not implement
* this standard.
*/
@NullMarked
package org.springframework.security.ldap.ppolicy;
import org.jspecify.annotations.NullMarked;
@@ -18,4 +18,7 @@
* {@code LdapUserSearch} implementations. These may be used to locate the user in the
* directory.
*/
@NullMarked
package org.springframework.security.ldap.search;
import org.jspecify.annotations.NullMarked;
@@ -24,8 +24,9 @@ import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldif.LDIFReader;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
@@ -35,6 +36,7 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.Lifecycle;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -43,7 +45,7 @@ import org.springframework.util.StringUtils;
public class UnboundIdContainer
implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware {
private InMemoryDirectoryServer directoryServer;
private @Nullable InMemoryDirectoryServer directoryServer;
private final String defaultPartitionSuffix;
@@ -51,13 +53,13 @@ public class UnboundIdContainer
private boolean isEphemeral;
private ConfigurableApplicationContext context;
private @Nullable ConfigurableApplicationContext context;
private boolean running;
private final String ldif;
private final @Nullable String ldif;
public UnboundIdContainer(String defaultPartitionSuffix, String ldif) {
public UnboundIdContainer(String defaultPartitionSuffix, @Nullable String ldif) {
this.defaultPartitionSuffix = defaultPartitionSuffix;
this.ldif = ldif;
}
@@ -84,7 +86,7 @@ public class UnboundIdContainer
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = (ConfigurableApplicationContext) applicationContext;
}
@@ -100,9 +102,11 @@ public class UnboundIdContainer
config.setEnforceSingleStructuralObjectClass(false);
config.setEnforceAttributeSyntaxCompliance(true);
DN dn = new DN(this.defaultPartitionSuffix);
RDN rdn = dn.getRDN();
Assert.notNull(rdn, "defaultPartitionSuffix cannot be the empty DN");
Entry entry = new Entry(dn);
entry.addAttribute("objectClass", "top", "domain", "extensibleObject");
entry.addAttribute("dc", dn.getRDN().getAttributeValues()[0]);
entry.addAttribute("dc", rdn.getAttributeValues()[0]);
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.add(entry);
importLdif(directoryServer);
@@ -118,6 +122,7 @@ public class UnboundIdContainer
private void importLdif(InMemoryDirectoryServer directoryServer) {
if (StringUtils.hasText(this.ldif)) {
Assert.notNull(this.context, "context cannot be null if ldif has a value");
try {
Resource[] resources = this.context.getResources(this.ldif);
if (resources.length > 0) {
@@ -140,7 +145,9 @@ public class UnboundIdContainer
if (this.isEphemeral && this.context != null && !this.context.isClosed()) {
return;
}
this.directoryServer.shutDown(true);
if (this.directoryServer != null) {
this.directoryServer.shutDown(true);
}
this.running = false;
}
@@ -17,4 +17,7 @@
/**
* Embedded UnboundID Server implementation, as used by the configuration namespace.
*/
@NullMarked
package org.springframework.security.ldap.server;
import org.jspecify.annotations.NullMarked;
@@ -18,6 +18,7 @@ package org.springframework.security.ldap.userdetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -29,6 +30,7 @@ import javax.naming.directory.SearchControls;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.ldap.core.ContextSource;
@@ -109,7 +111,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
/**
* A default role which will be assigned to all authenticated users if set
*/
private GrantedAuthority defaultRole;
private @Nullable GrantedAuthority defaultRole;
/**
* Template that will be used for searching
@@ -130,7 +132,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
/**
* The base DN from which the search for group membership should be performed
*/
private final String groupSearchBase;
private final @Nullable String groupSearchBase;
/**
* The pattern to be used for the user search. {0} is the user's DN
@@ -150,7 +152,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
/**
* The mapping function to be used to populate authorities.
*/
private Function<Map<String, List<String>>, GrantedAuthority> authorityMapper;
private Function<Map<String, List<String>>, @Nullable GrantedAuthority> authorityMapper;
/**
* Constructor for group search scenarios. <tt>userRoleAttributes</tt> may still be
@@ -159,7 +161,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
* @param groupSearchBase if this is an empty string the search will be performed from
* the root DN of the context factory. If null, no search will be performed.
*/
public DefaultLdapAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) {
public DefaultLdapAuthoritiesPopulator(ContextSource contextSource, @Nullable String groupSearchBase) {
Assert.notNull(contextSource, "contextSource must not be null");
this.ldapTemplate = new SpringSecurityLdapTemplate(contextSource);
getLdapTemplate().setSearchControls(getSearchControls());
@@ -176,9 +178,6 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
return null;
}
String role = roles.get(0);
if (role == null) {
return null;
}
if (this.convertToUpperCase) {
role = role.toUpperCase(Locale.ROOT);
}
@@ -196,7 +195,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
*/
protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {
return null;
return Collections.emptySet();
}
/**
@@ -223,15 +222,15 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
}
public Set<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) {
if (getGroupSearchBase() == null) {
String base = getGroupSearchBase();
if (base == null) {
return new HashSet<>();
}
Set<GrantedAuthority> authorities = new HashSet<>();
logger.trace(LogMessage.of(() -> "Searching for roles for user " + username + " with DN " + userDn
+ " and filter " + this.groupSearchFilter + " in search base " + getGroupSearchBase()));
Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues(
getGroupSearchBase(), this.groupSearchFilter, new String[] { userDn, username },
new String[] { this.groupRoleAttribute });
Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues(base,
this.groupSearchFilter, new String[] { userDn, username }, new String[] { this.groupRoleAttribute });
logger.debug(LogMessage.of(() -> "Found roles from search " + userRoles));
for (Map<String, List<String>> role : userRoles) {
GrantedAuthority authority = this.authorityMapper.apply(role);
@@ -246,7 +245,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
return getLdapTemplate().getContextSource();
}
protected String getGroupSearchBase() {
protected @Nullable String getGroupSearchBase() {
return this.groupSearchBase;
}
@@ -311,7 +310,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
* {@link GrantedAuthority} given the context record.
* @param authorityMapper the mapping function
*/
public void setAuthorityMapper(Function<Map<String, List<String>>, GrantedAuthority> authorityMapper) {
public void setAuthorityMapper(Function<Map<String, List<String>>, @Nullable GrantedAuthority> authorityMapper) {
Assert.notNull(authorityMapper, "authorityMapper must not be null");
this.authorityMapper = authorityMapper;
}
@@ -16,8 +16,13 @@
package org.springframework.security.ldap.userdetails;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.util.Assert;
/**
* UserDetails implementation whose properties are based on a subset of the LDAP schema
@@ -32,115 +37,115 @@ public class InetOrgPerson extends Person {
private static final long serialVersionUID = 620L;
private String carLicense;
private @Nullable String carLicense;
// Person.cn
private String destinationIndicator;
private @Nullable String destinationIndicator;
private String departmentNumber;
private @Nullable String departmentNumber;
// Person.description
private String displayName;
private @Nullable String displayName;
private String employeeNumber;
private @Nullable String employeeNumber;
private String homePhone;
private @Nullable String homePhone;
private String homePostalAddress;
private @Nullable String homePostalAddress;
private String initials;
private @Nullable String initials;
private String mail;
private @Nullable String mail;
private String mobile;
private @Nullable String mobile;
private String o;
private @Nullable String o;
private String ou;
private @Nullable String ou;
private String postalAddress;
private @Nullable String postalAddress;
private String postalCode;
private @Nullable String postalCode;
private String roomNumber;
private @Nullable String roomNumber;
private String street;
private @Nullable String street;
// Person.sn
// Person.telephoneNumber
private String title;
private @Nullable String title;
private String uid;
private @Nullable String uid;
public String getUid() {
return this.uid;
return Objects.requireNonNull(this.uid, "uid cannot be null");
}
public String getMail() {
public @Nullable String getMail() {
return this.mail;
}
public String getEmployeeNumber() {
public @Nullable String getEmployeeNumber() {
return this.employeeNumber;
}
public String getInitials() {
public @Nullable String getInitials() {
return this.initials;
}
public String getDestinationIndicator() {
public @Nullable String getDestinationIndicator() {
return this.destinationIndicator;
}
public String getO() {
public @Nullable String getO() {
return this.o;
}
public String getOu() {
public @Nullable String getOu() {
return this.ou;
}
public String getTitle() {
public @Nullable String getTitle() {
return this.title;
}
public String getCarLicense() {
public @Nullable String getCarLicense() {
return this.carLicense;
}
public String getDepartmentNumber() {
public @Nullable String getDepartmentNumber() {
return this.departmentNumber;
}
public String getDisplayName() {
public @Nullable String getDisplayName() {
return this.displayName;
}
public String getHomePhone() {
public @Nullable String getHomePhone() {
return this.homePhone;
}
public String getRoomNumber() {
public @Nullable String getRoomNumber() {
return this.roomNumber;
}
public String getHomePostalAddress() {
public @Nullable String getHomePostalAddress() {
return this.homePostalAddress;
}
public String getMobile() {
public @Nullable String getMobile() {
return this.mobile;
}
public String getPostalAddress() {
public @Nullable String getPostalAddress() {
return this.postalAddress;
}
public String getPostalCode() {
public @Nullable String getPostalCode() {
return this.postalCode;
}
public String getStreet() {
public @Nullable String getStreet() {
return this.street;
}
@@ -170,6 +175,8 @@ public class InetOrgPerson extends Person {
public static class Essence extends Person.Essence {
private @Nullable String username;
public Essence() {
}
@@ -214,7 +221,9 @@ public class InetOrgPerson extends Person {
setRoomNumber(ctx.getStringAttribute("roomNumber"));
setStreet(ctx.getStringAttribute("street"));
setTitle(ctx.getStringAttribute("title"));
setUid(ctx.getStringAttribute("uid"));
String uid = ctx.getStringAttribute("uid");
Assert.notNull(uid, "uid cannot be null");
setUid(uid);
}
@Override
@@ -222,79 +231,102 @@ public class InetOrgPerson extends Person {
return new InetOrgPerson();
}
public void setMail(String email) {
public void setMail(@Nullable String email) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).mail = email;
}
public void setUid(String uid) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).uid = uid;
if (this.instance.getUsername() == null) {
if (this.username == null) {
setUsername(uid);
}
}
public void setInitials(String initials) {
public void setUsername(String username) {
super.setUsername(username);
this.username = username;
}
public void setInitials(@Nullable String initials) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).initials = initials;
}
public void setO(String organization) {
public void setO(@Nullable String organization) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).o = organization;
}
public void setOu(String ou) {
public void setOu(@Nullable String ou) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).ou = ou;
}
public void setRoomNumber(String no) {
public void setRoomNumber(@Nullable String no) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).roomNumber = no;
}
public void setTitle(String title) {
public void setTitle(@Nullable String title) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).title = title;
}
public void setCarLicense(String carLicense) {
public void setCarLicense(@Nullable String carLicense) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).carLicense = carLicense;
}
public void setDepartmentNumber(String departmentNumber) {
public void setDepartmentNumber(@Nullable String departmentNumber) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).departmentNumber = departmentNumber;
}
public void setDisplayName(String displayName) {
public void setDisplayName(@Nullable String displayName) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).displayName = displayName;
}
public void setEmployeeNumber(String no) {
public void setEmployeeNumber(@Nullable String no) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).employeeNumber = no;
}
public void setDestinationIndicator(String destination) {
public void setDestinationIndicator(@Nullable String destination) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).destinationIndicator = destination;
}
public void setHomePhone(String homePhone) {
public void setHomePhone(@Nullable String homePhone) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).homePhone = homePhone;
}
public void setStreet(String street) {
public void setStreet(@Nullable String street) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).street = street;
}
public void setPostalCode(String postalCode) {
public void setPostalCode(@Nullable String postalCode) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).postalCode = postalCode;
}
public void setPostalAddress(String postalAddress) {
public void setPostalAddress(@Nullable String postalAddress) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).postalAddress = postalAddress;
}
public void setMobile(String mobile) {
public void setMobile(@Nullable String mobile) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).mobile = mobile;
}
public void setHomePostalAddress(String homePostalAddress) {
public void setHomePostalAddress(@Nullable String homePostalAddress) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((InetOrgPerson) this.instance).homePostalAddress = homePostalAddress;
}
@@ -21,6 +21,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
@@ -39,7 +41,7 @@ public class LdapAuthority implements GrantedAuthority {
private final String role;
private final Map<String, List<String>> attributes;
private final @Nullable Map<String, List<String>> attributes;
/**
* Constructs an LdapAuthority that has a role and a DN but no other attributes
@@ -56,7 +58,7 @@ public class LdapAuthority implements GrantedAuthority {
* @param dn the distinguished name
* @param attributes additional LDAP attributes
*/
public LdapAuthority(String role, String dn, Map<String, List<String>> attributes) {
public LdapAuthority(String role, String dn, @Nullable Map<String, List<String>> attributes) {
Assert.notNull(role, "role can not be null");
Assert.notNull(dn, "dn can not be null");
this.role = role;
@@ -68,7 +70,7 @@ public class LdapAuthority implements GrantedAuthority {
* Returns the LDAP attributes
* @return the LDAP attributes, map can be null
*/
public Map<String, List<String>> getAttributes() {
public @Nullable Map<String, List<String>> getAttributes() {
return this.attributes;
}
@@ -98,7 +100,7 @@ public class LdapAuthority implements GrantedAuthority {
* @param name the attribute name
* @return the first attribute value for a specified attribute, may be null
*/
public String getFirstAttributeValue(String name) {
public @Nullable String getFirstAttributeValue(String name) {
List<String> result = getAttributeValues(name);
return (!result.isEmpty()) ? result.get(0) : null;
}
@@ -20,9 +20,12 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.naming.Name;
import org.jspecify.annotations.Nullable;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
@@ -50,11 +53,11 @@ public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData
private static final long serialVersionUID = 620L;
private String dn;
private @Nullable String dn;
private String password;
private @Nullable String password;
private String username;
private @Nullable String username;
private Collection<GrantedAuthority> authorities = AuthorityUtils.NO_AUTHORITIES;
@@ -81,17 +84,17 @@ public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData
@Override
public String getDn() {
return this.dn;
return Objects.requireNonNull(this.dn, "dn cannot be null");
}
@Override
public String getPassword() {
public @Nullable String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
return Objects.requireNonNull(this.username, "username cannot be null");
}
@Override
@@ -132,6 +135,7 @@ public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData
@Override
public boolean equals(Object obj) {
if (obj instanceof LdapUserDetailsImpl) {
Assert.notNull(this.dn, "dn cannot be null");
return this.dn.equals(((LdapUserDetailsImpl) obj).dn);
}
return false;
@@ -139,6 +143,7 @@ public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData
@Override
public int hashCode() {
Assert.notNull(this.dn, "dn cannot be null");
return this.dn.hashCode();
}
@@ -163,7 +168,7 @@ public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData
*/
public static class Essence {
protected LdapUserDetailsImpl instance = createTarget();
protected @Nullable LdapUserDetailsImpl instance = createTarget();
private List<GrantedAuthority> mutableAuthorities = new ArrayList<>();
@@ -223,47 +228,58 @@ public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData
}
public void setAccountNonExpired(boolean accountNonExpired) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.accountNonLocked = accountNonLocked;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.mutableAuthorities = new ArrayList<>();
this.mutableAuthorities.addAll(authorities);
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.credentialsNonExpired = credentialsNonExpired;
}
public void setDn(String dn) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.dn = dn;
}
public void setDn(Name dn) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.dn = dn.toString();
}
public void setEnabled(boolean enabled) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.enabled = enabled;
}
public void setPassword(String password) {
public void setPassword(@Nullable String password) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.password = password;
}
public void setUsername(String username) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.username = username;
}
public void setTimeBeforeExpiration(int timeBeforeExpiration) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.timeBeforeExpiration = timeBeforeExpiration;
}
public void setGraceLoginsRemaining(int graceLoginsRemaining) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
this.instance.graceLoginsRemaining = graceLoginsRemaining;
}
@@ -42,6 +42,7 @@ import javax.naming.ldap.LdapName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.ldap.core.AttributesMapper;
@@ -129,7 +130,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
return new SimpleGrantedAuthority(this.rolePrefix + role.toUpperCase(Locale.ROOT));
};
private String[] attributesToRetrieve;
private String @Nullable [] attributesToRetrieve;
private boolean usePasswordModifyExtensionOperation = false;
@@ -186,7 +187,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
* @param newPassword the new value of the password.
*/
@Override
public void changePassword(final String oldPassword, final String newPassword) {
public void changePassword(final @Nullable String oldPassword, final @Nullable String newPassword) {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
Assert.notNull(authentication,
"No authentication object found in security context. Can't change current user's password!");
@@ -312,17 +313,23 @@ public class LdapUserDetailsManager implements UserDetailsManager {
this.template.executeReadWrite((ctx) -> {
for (GrantedAuthority authority : authorities) {
String group = convertAuthorityToGroup(authority);
if (group == null) {
continue;
}
LdapName fullDn = LdapUtils.getFullDn(userDn, ctx);
ModificationItem addGroup = new ModificationItem(modType,
new BasicAttribute(this.groupMemberAttributeName, fullDn.toString()));
ctx.modifyAttributes(buildGroupDn(group), new ModificationItem[] { addGroup });
}
return null;
return void.class;
});
}
private String convertAuthorityToGroup(GrantedAuthority authority) {
private @Nullable String convertAuthorityToGroup(GrantedAuthority authority) {
String group = authority.getAuthority();
if (group == null) {
return null;
}
if (group.startsWith(this.rolePrefix)) {
group = group.substring(this.rolePrefix.length());
}
@@ -424,7 +431,8 @@ public class LdapUserDetailsManager implements UserDetailsManager {
this.rolePrefix = rolePrefix;
}
private void changePasswordUsingAttributeModification(LdapName userDn, String oldPassword, String newPassword) {
private void changePasswordUsingAttributeModification(LdapName userDn, @Nullable String oldPassword,
@Nullable String newPassword) {
ModificationItem[] passwordChange = new ModificationItem[] { new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute(this.passwordAttributeName, newPassword)) };
if (oldPassword == null) {
@@ -444,11 +452,12 @@ public class LdapUserDetailsManager implements UserDetailsManager {
throw new BadCredentialsException("Authentication for password change failed.");
}
ctx.modifyAttributes(userDn, passwordChange);
return null;
return void.class;
});
}
private void changePasswordUsingExtensionOperation(LdapName userDn, String oldPassword, String newPassword) {
private void changePasswordUsingExtensionOperation(LdapName userDn, @Nullable String oldPassword,
@Nullable String newPassword) {
this.template.executeReadWrite((dirCtx) -> {
LdapContext ctx = (LdapContext) dirCtx;
String userIdentity = LdapUtils.getFullDn(userDn, ctx).toString();
@@ -491,7 +500,8 @@ public class LdapUserDetailsManager implements UserDetailsManager {
private final ByteArrayOutputStream value = new ByteArrayOutputStream();
PasswordModifyRequest(String userIdentity, String oldPassword, String newPassword) {
PasswordModifyRequest(@Nullable String userIdentity, @Nullable String oldPassword,
@Nullable String newPassword) {
ByteArrayOutputStream elements = new ByteArrayOutputStream();
if (userIdentity != null) {
berEncode(USER_IDENTITY_OCTET_TYPE, userIdentity.getBytes(), elements);
@@ -516,7 +526,7 @@ public class LdapUserDetailsManager implements UserDetailsManager {
}
@Override
public ExtendedResponse createExtendedResponse(String id, byte[] berValue, int offset, int length) {
public @Nullable ExtendedResponse createExtendedResponse(String id, byte[] berValue, int offset, int length) {
return null;
}
@@ -21,6 +21,7 @@ import java.util.Locale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.ldap.core.DirContextAdapter;
@@ -47,7 +48,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
private String rolePrefix = "ROLE_";
private String[] roleAttributes = null;
private String @Nullable [] roleAttributes = null;
private boolean convertToUpperCase = true;
@@ -125,7 +126,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper {
* @return the authority to be added to the list of authorities for the user, or null
* if this attribute should be ignored.
*/
protected GrantedAuthority createAuthority(Object role) {
protected @Nullable GrantedAuthority createAuthority(Object role) {
if (role instanceof String) {
if (this.convertToUpperCase) {
role = ((String) role).toUpperCase(Locale.ROOT);
@@ -20,6 +20,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.logging.Log;
@@ -128,7 +129,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
/**
* The attribute names to retrieve for each LDAP group
*/
private Set<String> attributeNames;
private Set<String> attributeNames = new HashSet<>();
/**
* Maximum search depth - represents the number of recursive searches performed
@@ -148,11 +149,12 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
@Override
public Set<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) {
if (getGroupSearchBase() == null) {
String base = getGroupSearchBase();
if (base == null) {
return new HashSet<>();
}
Set<GrantedAuthority> authorities = new HashSet<>();
performNestedSearch(userDn, username, authorities, getMaxSearchDepth());
performNestedSearch(base, userDn, username, authorities, getMaxSearchDepth());
return authorities;
}
@@ -164,7 +166,8 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
* @param authorities - the authorities set that will be populated, must not be null
* @param depth - the depth remaining, when 0 recursion will end
*/
private void performNestedSearch(String userDn, String username, Set<GrantedAuthority> authorities, int depth) {
private void performNestedSearch(String base, String userDn, String username, Set<GrantedAuthority> authorities,
int depth) {
if (depth == 0) {
// back out of recursion
logger.debug(LogMessage.of(() -> "Aborted search since max depth reached," + " for roles for user '"
@@ -174,19 +177,15 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
}
logger.trace(LogMessage.of(() -> "Searching for roles for user " + username + " with DN " + userDn
+ " and filter " + getGroupSearchFilter() + " in search base " + getGroupSearchBase()));
if (getAttributeNames() == null) {
setAttributeNames(new HashSet<>());
}
if (StringUtils.hasText(getGroupRoleAttribute())) {
getAttributeNames().add(getGroupRoleAttribute());
}
Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues(
getGroupSearchBase(), getGroupSearchFilter(), new String[] { userDn, username },
getAttributeNames().toArray(new String[0]));
Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues(base,
getGroupSearchFilter(), new String[] { userDn, username }, getAttributeNames().toArray(new String[0]));
logger.debug(LogMessage.format("Found roles from search %s", userRoles));
for (Map<String, List<String>> record : userRoles) {
boolean circular = false;
String dn = record.get(SpringSecurityLdapTemplate.DN_KEY).get(0);
String dn = Objects.requireNonNull(record.get(SpringSecurityLdapTemplate.DN_KEY)).get(0);
List<String> roleValues = record.get(getGroupRoleAttribute());
Set<String> roles = new HashSet<>();
if (roleValues != null) {
@@ -203,7 +202,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
}
String roleName = (!roles.isEmpty()) ? roles.iterator().next() : dn;
if (!circular) {
performNestedSearch(dn, roleName, authorities, (depth - 1));
performNestedSearch(base, dn, roleName, authorities, (depth - 1));
}
}
}
@@ -222,7 +221,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula
* @param attributeNames - the names of the LDAP attributes to retrieve
*/
public void setAttributeNames(Set<String> attributeNames) {
this.attributeNames = attributeNames;
this.attributeNames = (attributeNames != null) ? attributeNames : new HashSet<>();
}
/**
@@ -20,6 +20,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.ldap.LdapUtils;
@@ -36,24 +38,24 @@ public class Person extends LdapUserDetailsImpl {
private static final long serialVersionUID = 620L;
private String givenName;
private @Nullable String givenName;
private String sn;
private @Nullable String sn;
private String description;
private @Nullable String description;
private String telephoneNumber;
private @Nullable String telephoneNumber;
private List<String> cn = new ArrayList<>();
protected Person() {
}
public String getGivenName() {
public @Nullable String getGivenName() {
return this.givenName;
}
public String getSn() {
public @Nullable String getSn() {
return this.sn;
}
@@ -61,11 +63,11 @@ public class Person extends LdapUserDetailsImpl {
return this.cn.toArray(new String[0]);
}
public String getDescription() {
public @Nullable String getDescription() {
return this.description;
}
public String getTelephoneNumber() {
public @Nullable String getTelephoneNumber() {
return this.telephoneNumber;
}
@@ -88,7 +90,9 @@ public class Person extends LdapUserDetailsImpl {
public Essence(DirContextOperations ctx) {
super(ctx);
setCn(ctx.getStringAttributes("cn"));
String[] cns = ctx.getStringAttributes("cn");
cns = (cns != null) ? cns : new String[0];
setCn(cns);
setGivenName(ctx.getStringAttribute("givenName"));
setSn(ctx.getStringAttribute("sn"));
setDescription(ctx.getStringAttribute("description"));
@@ -105,7 +109,7 @@ public class Person extends LdapUserDetailsImpl {
setSn(copyMe.sn);
setDescription(copyMe.getDescription());
setTelephoneNumber(copyMe.getTelephoneNumber());
((Person) this.instance).cn = new ArrayList<>(copyMe.cn);
setCn(copyMe.cn.toArray(String[]::new));
}
@Override
@@ -113,27 +117,33 @@ public class Person extends LdapUserDetailsImpl {
return new Person();
}
public void setGivenName(String givenName) {
public void setGivenName(@Nullable String givenName) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((Person) this.instance).givenName = givenName;
}
public void setSn(String sn) {
public void setSn(@Nullable String sn) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((Person) this.instance).sn = sn;
}
public void setCn(String[] cn) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((Person) this.instance).cn = Arrays.asList(cn);
}
public void addCn(String value) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((Person) this.instance).cn.add(value);
}
public void setTelephoneNumber(String tel) {
public void setTelephoneNumber(@Nullable String tel) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((Person) this.instance).telephoneNumber = tel;
}
public void setDescription(String desc) {
public void setDescription(@Nullable String desc) {
Assert.notNull(this.instance, "Essence can only be used to create a single instance");
((Person) this.instance).description = desc;
}
@@ -18,4 +18,7 @@
* LDAP-focused {@code UserDetails} implementations which map from a ubset of the data
* contained in some of the standard LDAP types (such as {@code InetOrgPerson}).
*/
@NullMarked
package org.springframework.security.ldap.userdetails;
import org.jspecify.annotations.NullMarked;
@@ -55,6 +55,15 @@ public class SpringSecurityAuthenticationSourceTests {
assertThat(source.getCredentials()).isEqualTo("");
}
@Test
public void credentialsAreEmptyWithNullCredentials() {
AuthenticationSource source = new SpringSecurityAuthenticationSource();
SecurityContextHolder.getContext()
.setAuthentication(
new AnonymousAuthenticationToken("key", "anonUser", AuthorityUtils.createAuthorityList("ignored")));
assertThat(source.getCredentials()).isEqualTo("");
}
@Test
public void principalIsEmptyForAnonymousUser() {
AuthenticationSource source = new SpringSecurityAuthenticationSource();
@@ -18,6 +18,7 @@ package org.springframework.security.ldap.authentication;
import java.util.Collection;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.Test;
import org.springframework.ldap.CommunicationException;
@@ -168,6 +169,7 @@ public class LdapAuthenticationProviderTests {
SecurityAssertions.assertThat(result).hasAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY);
}
@NullMarked
class MockAuthenticator implements LdapAuthenticator {
@Override
@@ -16,15 +16,20 @@
package org.springframework.security.ldap.authentication;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.ldap.search.LdapUserSearch;
/**
* @author Luke Taylor
*/
@NullMarked
public class MockUserSearch implements LdapUserSearch {
DirContextOperations user;
@Nullable DirContextOperations user;
public MockUserSearch() {
}
@@ -35,6 +40,9 @@ public class MockUserSearch implements LdapUserSearch {
@Override
public DirContextOperations searchForUser(String username) {
if (this.user == null) {
throw UsernameNotFoundException.fromUsername(username);
}
return this.user;
}
@@ -19,6 +19,8 @@ package org.springframework.security.ldap.userdetails;
import java.util.Collection;
import java.util.Set;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullUnmarked;
import org.junit.jupiter.api.Test;
import org.springframework.ldap.core.DirContextAdapter;
@@ -71,10 +73,12 @@ public class LdapUserDetailsServiceTests {
assertThat(user.getAuthorities()).isEmpty();
}
@NullUnmarked
class MockAuthoritiesPopulator implements LdapAuthoritiesPopulator {
@Override
public Collection<GrantedAuthority> getGrantedAuthorities(DirContextOperations userCtx, String username) {
public Collection<GrantedAuthority> getGrantedAuthorities(@NonNull DirContextOperations userCtx,
String username) {
return AuthorityUtils.createAuthorityList("ROLE_FROM_POPULATOR");
}