Skip to content

Commit 8353ad4

Browse files
committed
refactor(active-directory): unify username handling with suffix and DN support
fix warnings
1 parent 4c25593 commit 8353ad4

3 files changed

Lines changed: 79 additions & 51 deletions

File tree

core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.shiro.authc.UsernamePasswordToken;
2525
import org.apache.shiro.authz.AuthorizationInfo;
2626
import org.apache.shiro.authz.SimpleAuthorizationInfo;
27+
import org.apache.shiro.lang.util.StringUtils;
2728
import org.apache.shiro.realm.Realm;
2829
import org.apache.shiro.realm.ldap.AbstractLdapRealm;
2930
import org.apache.shiro.realm.ldap.LdapContextFactory;
@@ -110,7 +111,7 @@ protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken toke
110111
// Binds using the username and password provided by the user.
111112
LdapContext ctx = null;
112113
try {
113-
ctx = ldapContextFactory.getLdapContext(getUsernameForAuthentication(upToken.getUsername()),
114+
ctx = ldapContextFactory.getLdapContext(getUsernameWithSuffixOrFullDN(upToken.getUsername()),
114115
String.valueOf(upToken.getPassword()));
115116
} finally {
116117
LdapUtils.closeContext(ctx);
@@ -169,7 +170,7 @@ protected Set<String> getRoleNamesForUser(String username, LdapContext ldapConte
169170
SearchControls searchControls = new SearchControls();
170171
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
171172

172-
String userPrincipalName = getUsernameForAuthentication(username);
173+
String userPrincipalName = getUsernameWithSuffixOrFullDN(username);
173174

174175
Object[] searchArguments = new Object[] {userPrincipalName};
175176

@@ -235,6 +236,43 @@ protected Collection<String> getRoleNamesForGroups(Collection<String> groupNames
235236
return roleNames;
236237
}
237238

239+
/**
240+
* Returns the username to use for authentication.
241+
* If {@link #principalSuffix} is configured, the sanitized username with appended suffix will be returned.
242+
* If {@link #principalSuffix} is not configured, the method will check if the username is a valid LDAP DN.
243+
* If it is a valid LDAP DN, the username will be returned as-is,
244+
* otherwise the sanitized username with appended suffix will be returned.
245+
*
246+
* @param username input to check and sanitize
247+
* @return the sanitized username with optional suffix to use for authentication
248+
*/
249+
protected String getUsernameWithSuffixOrFullDN(String username) {
250+
if (!StringUtils.hasText(principalSuffix)) {
251+
try {
252+
LdapName ldapName = new LdapName(username);
253+
// Full LDAP DN needs to have more than one RDN, so we can return the username as-is
254+
if (ldapName.size() > 1) {
255+
return username;
256+
}
257+
} catch (javax.naming.InvalidNameException e) {
258+
// Not a valid LDAP DN, so treat it as a regular username.
259+
}
260+
}
261+
262+
return getUsernameWithSuffix(username);
263+
}
264+
265+
/**
266+
* Returns the sanitized username with appended suffix if {@link #principalSuffix} is configured
267+
* and the username does not already end with it.
268+
* If {@link #principalSuffix} is not configured, the sanitized username will be returned
269+
* <p>
270+
* NOTE: {@link #getUsernameWithSuffixOrFullDN(String)} should be used instead of this method
271+
* to handle full LDAP DNs correctly.
272+
*
273+
* @param username input to sanitize and append suffix
274+
* @return the sanitized username with optional suffix
275+
*/
238276
protected String getUsernameWithSuffix(String username) {
239277
String sanitizedUsername = Rdn.escapeValue(username);
240278
if (principalSuffix != null
@@ -243,18 +281,4 @@ protected String getUsernameWithSuffix(String username) {
243281
}
244282
return sanitizedUsername;
245283
}
246-
247-
protected String getUsernameForAuthentication(String username) {
248-
try {
249-
LdapName ldapName = new LdapName(username);
250-
if (ldapName.size() > 1) {
251-
return username;
252-
}
253-
} catch (javax.naming.InvalidNameException e) {
254-
// Not a valid LDAP DN, so treat it as a regular username.
255-
}
256-
257-
return getUsernameWithSuffix(username);
258-
}
259-
260284
}

core/src/test/java/org/apache/shiro/mgt/AbstractRememberMeManagerObjectInputFilterTest.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,21 @@
1919
package org.apache.shiro.mgt;
2020

2121
import org.apache.shiro.lang.io.Serializer;
22+
import org.apache.shiro.subject.ImmutablePrincipalCollection;
2223
import org.apache.shiro.subject.PrincipalCollection;
23-
import org.apache.shiro.subject.SimplePrincipalCollection;
2424
import org.apache.shiro.subject.Subject;
2525
import org.apache.shiro.subject.SubjectContext;
2626
import org.apache.shiro.subject.support.DefaultSubjectContext;
2727
import org.junit.jupiter.api.Test;
2828

29+
import java.io.ByteArrayInputStream;
2930
import java.io.ByteArrayOutputStream;
3031
import java.io.IOException;
3132
import java.io.InvalidClassException;
3233
import java.io.ObjectInputFilter;
34+
import java.io.ObjectInputStream;
3335
import java.io.ObjectOutputStream;
36+
import java.io.Serial;
3437
import java.io.Serializable;
3538
import java.util.ArrayList;
3639
import java.util.List;
@@ -51,7 +54,7 @@ class AbstractRememberMeManagerObjectInputFilterTest {
5154
@Test
5255
void testLegitimatePrincipalsRoundTripUnderDefaultFilter() {
5356
InMemoryRememberMeManager rmm = new InMemoryRememberMeManager();
54-
PrincipalCollection principals = new SimplePrincipalCollection("joecool", "myRealm");
57+
PrincipalCollection principals = ImmutablePrincipalCollection.ofSinglePrincipal("joecool", "myRealm");
5558

5659
rmm.rememberIdentity(null, principals);
5760
PrincipalCollection remembered = rmm.getRememberedPrincipals(new DefaultSubjectContext());
@@ -95,18 +98,18 @@ void testDefaultFilterRejectsOversizedPayloadBeforeFullConstruction() {
9598
@Test
9699
void testCustomStricterAllowListFilterCanBeConfigured() {
97100
// Documented override path (see AbstractRememberMeManager#getSerializer javadoc): replace the default
98-
// serializer's filter with a strict class allow-list. Only SimplePrincipalCollection,
101+
// serializer's filter with a strict class allow-list. Only ImmutablePrincipalCollection,
99102
// AbstractRememberMeManager.RememberedIdentity, and JDK collection/primitive/java.time plumbing are let
100103
// through. Note: java.time types (e.g. Instant) don't serialize themselves directly - they writeReplace()
101104
// to an internal java.time serialization proxy class, which is what actually appears in the stream.
102105
InMemoryRememberMeManager rmm = new InMemoryRememberMeManager();
103106
rmm.getSerializer()
104107
.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
105-
"org.apache.shiro.subject.SimplePrincipalCollection;"
108+
"org.apache.shiro.subject.ImmutablePrincipalCollection;"
106109
+ "org.apache.shiro.mgt.AbstractRememberMeManager$RememberedIdentity;"
107110
+ "java.time.*;java.util.*;java.lang.*;!*"));
108111

109-
PrincipalCollection principals = new SimplePrincipalCollection("joecool", "myRealm");
112+
PrincipalCollection principals = ImmutablePrincipalCollection.ofSinglePrincipal("joecool", "myRealm");
110113
rmm.rememberIdentity(null, principals);
111114
PrincipalCollection remembered = rmm.getRememberedPrincipals(new DefaultSubjectContext());
112115
assertThat(remembered.getPrimaryPrincipal()).isEqualTo("joecool");
@@ -126,25 +129,24 @@ void testCustomSerializerIsUnaffectedByDefaultFilterMachinery() {
126129
// A caller-supplied Serializer implementation (not a DefaultSerializer) must keep working exactly as
127130
// before this feature existed - AbstractRememberMeManager only touches the filter on its own default
128131
// DefaultSerializer instance, never on a replaced Serializer.
129-
InMemoryRememberMeManager rmm = new InMemoryRememberMeManager();
130-
rmm.setSerializer(new Serializer<AbstractRememberMeManager.RememberedIdentity>() {
132+
var rmm = new InMemoryRememberMeManager();
133+
rmm.setSerializer(new Serializer<>() {
131134
@Override
132135
public byte[] serialize(AbstractRememberMeManager.RememberedIdentity o) {
133136
return plainJdkSerialize(o);
134137
}
135138

136139
@Override
137-
@SuppressWarnings("unchecked")
138140
public AbstractRememberMeManager.RememberedIdentity deserialize(byte[] serialized) {
139-
try (var ois = new java.io.ObjectInputStream(new java.io.ByteArrayInputStream(serialized))) {
141+
try (var ois = new ObjectInputStream(new ByteArrayInputStream(serialized))) {
140142
return (AbstractRememberMeManager.RememberedIdentity) ois.readObject();
141143
} catch (IOException | ClassNotFoundException e) {
142144
throw new RuntimeException(e);
143145
}
144146
}
145147
});
146148

147-
PrincipalCollection principals = new SimplePrincipalCollection("joecool", "myRealm");
149+
PrincipalCollection principals = ImmutablePrincipalCollection.ofSinglePrincipal("joecool", "myRealm");
148150
rmm.rememberIdentity(null, principals);
149151
PrincipalCollection remembered = rmm.getRememberedPrincipals(new DefaultSubjectContext());
150152

@@ -164,6 +166,7 @@ private static byte[] plainJdkSerialize(Object o) {
164166
}
165167

166168
public static class NotAllowlisted implements Serializable {
169+
@Serial
167170
private static final long serialVersionUID = 1L;
168171
}
169172

core/src/test/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealmTest.java

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import javax.naming.directory.SearchControls;
5656
import javax.naming.directory.SearchResult;
5757
import javax.naming.ldap.LdapContext;
58+
import javax.naming.ldap.Rdn;
5859
import java.util.HashSet;
5960
import java.util.Set;
6061

@@ -153,30 +154,37 @@ void testInitialization() {
153154
}
154155

155156
@Test
156-
void testUsernameForAuthenticationWithDn() {
157-
ActiveDirectoryRealm activeDirectoryRealm = new ActiveDirectoryRealm() {{
158-
this.principalSuffix = "@example.com";
159-
}};
157+
void usernameForAuthenticationWithDnAndSuffix() {
158+
var activeDirectoryRealm = new ActiveDirectoryRealm();
159+
activeDirectoryRealm.setPrincipalSuffix("@example.com");
160160

161161
String dn = "CN=my_name,OU=Development,OU=Special Accounts,DC=mycompany,DC=com";
162162

163-
assertThat(activeDirectoryRealm.getUsernameForAuthentication(dn))
163+
assertThat(activeDirectoryRealm.getUsernameWithSuffixOrFullDN(dn))
164+
.isEqualTo(Rdn.escapeValue(dn) + "@example.com");
165+
}
166+
167+
@Test
168+
void usernameForAuthenticationWithDn() {
169+
var activeDirectoryRealm = new ActiveDirectoryRealm();
170+
171+
String dn = "CN=my_name,OU=Development,OU=Special Accounts,DC=mycompany,DC=com";
172+
173+
assertThat(activeDirectoryRealm.getUsernameWithSuffixOrFullDN(dn))
164174
.isEqualTo(dn);
165175
}
166176

167177
@Test
168178
void testUsernameForAuthenticationWithUsername() {
169-
ActiveDirectoryRealm activeDirectoryRealm = new ActiveDirectoryRealm();
179+
var activeDirectoryRealm = new ActiveDirectoryRealm();
170180

171-
assertThat(activeDirectoryRealm.getUsernameForAuthentication("test,user"))
181+
assertThat(activeDirectoryRealm.getUsernameWithSuffixOrFullDN("test,user"))
172182
.isEqualTo("test\\,user");
173183
}
174184

175185
@Test
176-
void testAuthenticationUsesDnWithoutEscaping() throws Exception {
177-
ActiveDirectoryRealm activeDirectoryRealm = new ActiveDirectoryRealm() {{
178-
this.principalSuffix = "@example.com";
179-
}};
186+
void authenticationUsesDnWithoutEscaping() throws Exception {
187+
var activeDirectoryRealm = new ActiveDirectoryRealm();
180188
LdapContextFactory factory = createMock(LdapContextFactory.class);
181189
LdapContext ldapContext = createNiceMock(LdapContext.class);
182190

@@ -193,10 +201,8 @@ void testAuthenticationUsesDnWithoutEscaping() throws Exception {
193201
}
194202

195203
@Test
196-
void testAuthorizationUsesDnWithoutEscaping() throws Exception {
197-
ActiveDirectoryRealm activeDirectoryRealm = new ActiveDirectoryRealm() {{
198-
this.principalSuffix = "@example.com";
199-
}};
204+
void authorizationUsesDnWithoutEscaping() throws Exception {
205+
var activeDirectoryRealm = new ActiveDirectoryRealm();
200206

201207
LdapContext ldapContext = createNiceMock(LdapContext.class);
202208
NamingEnumeration<SearchResult> results = createNiceMock(NamingEnumeration.class);
@@ -232,9 +238,8 @@ public void assertExistingUserSuffix(String username, String expectedPrincipalNa
232238
.andReturn(results);
233239
replay(ldapContext);
234240

235-
ActiveDirectoryRealm activeDirectoryRealm = new ActiveDirectoryRealm() {{
236-
this.principalSuffix = "@ExAmple.COM";
237-
}};
241+
var activeDirectoryRealm = new ActiveDirectoryRealm();
242+
activeDirectoryRealm.setPrincipalSuffix("@ExAmple.COM");
238243

239244
SecurityManager securityManager = new DefaultSecurityManager(activeDirectoryRealm);
240245
Subject subject = new Subject.Builder(securityManager).buildSubject();
@@ -277,10 +282,6 @@ public Optional<AuthenticationInfo> createSimulatedCredentials() {
277282
setCredentialsMatcher(credentialsMatcher);
278283
}
279284

280-
public void setPrincipalSuffix(String principalSuffix) {
281-
this.principalSuffix = principalSuffix;
282-
}
283-
284285
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
285286
SimpleAccount account = (SimpleAccount) super.doGetAuthenticationInfo(token);
286287

@@ -296,14 +297,14 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
296297
}
297298

298299
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
299-
Set<String> roles = new HashSet<String>();
300+
Set<String> roles = new HashSet<>();
300301
roles.add(ROLE);
301302
return new SimpleAuthorizationInfo(roles);
302303
}
303304

304305
// override ldap query because i don't care about testing that piece in this case
305-
protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory)
306-
throws NamingException {
306+
protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token,
307+
LdapContextFactory ldapContextFactory) {
307308
return new SimpleAccount(token.getPrincipal(), token.getCredentials(), getName());
308309
}
309310
}

0 commit comments

Comments
 (0)