(tomcat) branch 11.0.x updated: Improve handling of userRoleAttribute
[email protected] Wed, 29 Jul 2026 15:08:44 +0000
| Newsgroups | gmane.comp.jakarta.tomcat.devel |
|---|---|
| Message-ID | <178533772466.1072727.1575335207577215195@gitbox3-he-fi.apache.org> |
This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push:
new b8fd3e7c27 Improve handling of userRoleAttribute
b8fd3e7c27 is described below
commit b8fd3e7c27c1f1ac1c6ba5ec28e0f30fbfa6ba98
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Jul 29 16:00:32 2026 +0100
Improve handling of userRoleAttribute
Handle the case where the JNDIRealm is configured to perform role
searches with userRoleAttribute but the attribute is not available or
not configured for the current user.
---
java/org/apache/catalina/realm/JNDIRealm.java | 43 ++++++++++--
.../catalina/realm/TestJNDIRealmIntegration.java | 76 ++++++++++++++++++++++
webapps/docs/changelog.xml | 6 ++
3 files changed, 121 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java
index 940a9ba9c5..6ced388ac7 100644
--- a/java/org/apache/catalina/realm/JNDIRealm.java
+++ b/java/org/apache/catalina/realm/JNDIRealm.java
@@ -145,15 +145,18 @@ public class JNDIRealm extends RealmBase {
*/
public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases";
+ // Note: Usage assumes this value is escaped / doesn't need escaping
+ private static final String NULL_USER_ROLE_ATTRIBUTE_PLACEHOLDER = "tomcat-unset-ignore";
+
+ private static final String AUTHENTICATION_NAME_GSSAPI = "GSSAPI";
+
+
/**
* Constructs a new JNDIRealm.
*/
public JNDIRealm() {
}
- private static final String AUTHENTICATION_NAME_GSSAPI = "GSSAPI";
-
-
/**
* The type of authentication to use
*/
@@ -2093,6 +2096,29 @@ public class JNDIRealm extends RealmBase {
return list;
}
+ /*
+ * If the userRoleAttribute is unavailable (not configured or not set) for the current user, it would be more
+ * efficient to skip any role search using that attribute. However, filters can use boolean expressions and
+ * there is no standard Java API to parse a filter string into an abstract syntax tree. So, without writing a
+ * custom parser / importing a parser library, there is no way to determine if the filter is unaffected by the
+ * unavailable attribute, is a NO-OP if the attribute is unavailable or if the filter could be more efficiently
+ * expressed knowing the attribute is unavailable.
+ *
+ * Therefore, the solution chosen is to allow the lookup with a potentially inefficient filter to proceed.
+ *
+ * If userRoleId is left as null, that will result in it being treated as "null" by the filter. While it is
+ * considered that a role with a name containing "null" would not be present in the directory it is
+ * theoretically possible which would lead to the role being incorrectly included in the results. Therefore,
+ * userRoleId is modified to "tomcat-unset-ignore" as it is even less likely that a role with a name containing
+ * that string would exist. In the extremely unlikely event a role with such a name is returned, it is removed
+ * from the results below.
+ */
+ boolean placeholderUsed = false;
+ if (userRoleId == null && connection.roleFormatUsesUserRoleAttribute) {
+ userRoleId = NULL_USER_ROLE_ATTRIBUTE_PLACEHOLDER;
+ placeholderUsed = true;
+ }
+
// Set up parameters for an appropriate search filter
// The dn is already attribute value escaped but the others are not
// This is a filter so all input will require filter escaping
@@ -2140,7 +2166,9 @@ public class JNDIRealm extends RealmBase {
}
String dname = getDistinguishedName(connection.context, base, result);
String name = getAttributeValue(roleName, attrs);
- if (name != null && dname != null) {
+ // Also filters out roles with names containing NULL_USER_ROLE_ATTRIBUTE_PLACEHOLDER - see above
+ if (name != null && dname != null &&
+ !(placeholderUsed && dname.contains(NULL_USER_ROLE_ATTRIBUTE_PLACEHOLDER))) {
groupMap.put(dname, name);
}
}
@@ -3317,6 +3345,11 @@ public class JNDIRealm extends RealmBase {
*/
public final MessageFormat roleFormat;
+ /**
+ * A flag that indicates that roleSearch includes a reference to the {2} placeholder.
+ */
+ public final boolean roleFormatUsesUserRoleAttribute;
+
/**
* The directory context linking us to our directory server.
*/
@@ -3356,8 +3389,10 @@ public class JNDIRealm extends RealmBase {
if (roleSearch == null) {
roleFormat = null;
+ roleFormatUsesUserRoleAttribute = false;
} else {
roleFormat = new MessageFormat(roleSearch);
+ roleFormatUsesUserRoleAttribute = roleSearch.contains("{2}");
}
}
}
diff --git a/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java b/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java
index 6447247579..060a95fe52 100644
--- a/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java
+++ b/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java
@@ -51,8 +51,15 @@ public class TestJNDIRealmIntegration {
private static final String ROLE_SEARCH_A = "member={0}";
private static final String ROLE_SEARCH_B = "member=cn={1},ou=people,dc=example,dc=com";
private static final String ROLE_SEARCH_C = "member=cn={2},ou=people,dc=example,dc=com";
+ private static final String ROLE_SEARCH_D = "(|(member={0})(member=cn={2},ou=people,dc=example,dc=com))";
private static final String ROLE_BASE = "ou=people,dc=example,dc=com";
+ /*
+ * An attribute that is valid for the user entries used by these tests but that none of them actually has, so the
+ * value for {2} in the role search is unavailable even though userRoleAttribute is configured.
+ */
+ private static final String USER_ROLE_ATTRIBUTE_ABSENT = "ou";
+
private static InMemoryDirectoryServer ldapServer;
@Parameterized.Parameters(name = "{index}: user[{5}], pwd[{6}]")
@@ -74,6 +81,25 @@ public class TestJNDIRealmIntegration {
"{3},ou=people,dc=example,dc=com", "testsub", "test", new String[] { "TestGroup4" },
userRoleAttribute, Integer.valueOf(4) });
}
+ /*
+ * Role searches that use {2} - the value of userRoleAttribute - when no value is available for the user,
+ * either because userRoleAttribute is not configured or because the user's entry does not have that attribute.
+ * See the additional directory entries added by createLDAP() for these tests.
+ */
+ for (String userRoleAttribute : new String[] { null, USER_ROLE_ATTRIBUTE_ABSENT }) {
+ for (int poolSize : new int[] { 1, 4 }) {
+ // The role search can only match via {2} so no roles are expected
+ parameterSets.add(new Object[] { USER_PATTERN, null, null, ROLE_SEARCH_C, ROLE_BASE,
+ "test", "test", new String[0], userRoleAttribute, Integer.valueOf(poolSize) });
+ parameterSets.add(new Object[] { null, USER_SEARCH, USER_BASE, ROLE_SEARCH_C, ROLE_BASE,
+ "test", "test", new String[0], userRoleAttribute, Integer.valueOf(poolSize) });
+ // The role search can also match via {0} so the roles found that way are still expected
+ parameterSets.add(new Object[] { USER_PATTERN, null, null, ROLE_SEARCH_D, ROLE_BASE,
+ "test", "test", new String[] { "TestGroup" }, userRoleAttribute, Integer.valueOf(poolSize) });
+ parameterSets.add(new Object[] { null, USER_SEARCH, USER_BASE, ROLE_SEARCH_D, ROLE_BASE,
+ "test", "test", new String[] { "TestGroup" }, userRoleAttribute, Integer.valueOf(poolSize) });
+ }
+ }
return parameterSets;
}
@@ -304,6 +330,56 @@ public class TestJNDIRealmIntegration {
"userPassword: <>+=\"#;,rrr");
result = conn.processOperation(addUserBug65373);
Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+ /*
+ * The following entries exist so the role searches that use {2} have something they could match when no
+ * value is available for the user. They must not appear in the roles returned for any user.
+ */
+
+ // A role search that used the unavailable value directly would look for "cn=null" so make sure such an
+ // entry exists and is a member of a role
+ AddRequest addUserNull = new AddRequest(
+ "dn: cn=null,ou=people,dc=example,dc=com",
+ "objectClass: top",
+ "objectClass: person",
+ "objectClass: organizationalPerson",
+ "cn: null",
+ "sn: Null",
+ "userPassword: test");
+ result = conn.processOperation(addUserNull);
+ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+ AddRequest addGroupNull = new AddRequest(
+ "dn: cn=NullRoleGroup,ou=people,dc=example,dc=com",
+ "objectClass: top",
+ "objectClass: groupOfNames",
+ "cn: NullRoleGroup",
+ "member: cn=null,ou=people,dc=example,dc=com");
+ result = conn.processOperation(addGroupNull);
+ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+ // The role search uses the placeholder JNDIRealm substitutes for the unavailable value, so make sure an
+ // entry it matches exists. The name of the role contains the placeholder so JNDIRealm must remove it from
+ // the search results. Note: this deliberately depends on the placeholder value used by JNDIRealm.
+ AddRequest addUserPlaceholder = new AddRequest(
+ "dn: cn=tomcat-unset-ignore,ou=people,dc=example,dc=com",
+ "objectClass: top",
+ "objectClass: person",
+ "objectClass: organizationalPerson",
+ "cn: tomcat-unset-ignore",
+ "sn: Placeholder",
+ "userPassword: test");
+ result = conn.processOperation(addUserPlaceholder);
+ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+ AddRequest addGroupPlaceholder = new AddRequest(
+ "dn: cn=tomcat-unset-ignore-group,ou=people,dc=example,dc=com",
+ "objectClass: top",
+ "objectClass: groupOfNames",
+ "cn: tomcat-unset-ignore-group",
+ "member: cn=tomcat-unset-ignore,ou=people,dc=example,dc=com");
+ result = conn.processOperation(addGroupPlaceholder);
+ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
}
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index c507d6762f..52eb969b15 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -161,6 +161,12 @@
Separate the <code>Context</code> role mapping from the Servlet
specification <code>security-role-ref</code>. (remm)
</update>
+ <fix>
+ Handle the case where the <code>JNDIRealm</code> is configured to
+ perform role searches with <code>userRoleAttribute</code> but the
+ attribute is not available or not configured for the current user.
+ (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">