Skip to content

Commit d91b363

Browse files
jongyoulclaude
andcommitted
[MINOR] Address review feedback on LDAP escaping changes
- Apply RFC 4515 escaping to the matching-rule-in-chain filter site (groupObjectClass, memberAttribute, userDn) — previously this branch used String.format with no escape on values that may include parts of the principal once group membership is resolved through the chain. - Preserve the bare-placeholder DN template behaviour: when userDnTemplate is exactly "{0}", treat the principal as a full DN supplied verbatim rather than running RFC 4514 escape on it. Escaping collapsed comma-separated DNs into a single RDN value and broke binds for deployments that rely on full-DN principals. - Stop applying RFC 4514 escape inside setUserSearchFilter and setGroupSearchFilter. The values are operator-supplied filter templates; the placeholder substitution itself is the only place that needs escaping, and that already happens at expandFilterTemplate time. - Update LdapRealmTest expectations to reflect the verbatim template storage and the bare-placeholder passthrough. - Switch the second-call-site coverage in ActiveDirectoryGroupRealmFilterInjectionTest to actually invoke getRoleNamesForUser via reflection rather than re-running searchForUserName. - Drop the unused assertFalse imports flagged by Checkstyle and replace the raw NUL byte in LdapRealmDnInjectionTest with the Java escape sequence to keep the source file plain text. - Tighten the inline comment on the bare-placeholder branch and remove '=' from the expandDnTemplate Javadoc since RFC 4514 does not require escaping '=' inside an attribute value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5156af1 commit d91b363

5 files changed

Lines changed: 33 additions & 29 deletions

File tree

zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,10 @@ protected Set<String> rolesFor(PrincipalCollection principals, String userNameIn
363363
searchResultEnum = ldapCtx.search(
364364
getGroupSearchBase(),
365365
String.format(
366-
MATCHING_RULE_IN_CHAIN_FORMAT, groupObjectClass, memberAttribute, userDn),
366+
MATCHING_RULE_IN_CHAIN_FORMAT,
367+
LdapFilterEncoder.escapeFilterValue(groupObjectClass),
368+
LdapFilterEncoder.escapeFilterValue(memberAttribute),
369+
LdapFilterEncoder.escapeFilterValue(userDn)),
367370
searchControls);
368371
while (searchResultEnum != null && searchResultEnum.hasMore()) {
369372
// searchResults contains all the groups in search scope
@@ -774,15 +777,15 @@ public String getUserSearchFilter() {
774777
}
775778

776779
public void setUserSearchFilter(final String filter) {
777-
this.userSearchFilter = (filter == null ? null : escapeAttributeValue(filter.trim()));
780+
this.userSearchFilter = (filter == null ? null : filter.trim());
778781
}
779782

780783
public String getGroupSearchFilter() {
781784
return groupSearchFilter;
782785
}
783786

784787
public void setGroupSearchFilter(final String filter) {
785-
this.groupSearchFilter = (filter == null ? null : escapeAttributeValue(filter.trim()));
788+
this.groupSearchFilter = (filter == null ? null : filter.trim());
786789
}
787790

788791
public boolean getUserLowerCase() {
@@ -887,7 +890,13 @@ protected String getUserDn(final String principal) throws IllegalArgumentExcepti
887890
// If not searching use the userDnTemplate and return.
888891
if ((userSearchBase == null || userSearchBase.isEmpty()) || (userSearchAttributeName == null
889892
&& userSearchFilter == null && !"object".equalsIgnoreCase(userSearchScope))) {
890-
userDn = expandDnTemplate(userDnTemplate, matchedPrincipal);
893+
// Bare-placeholder template: principal is already a full DN; escaping
894+
// its commas would collapse the RDNs and break binds.
895+
if (MEMBER_SUBSTITUTION_TOKEN.equals(userDnTemplate)) {
896+
userDn = matchedPrincipal;
897+
} else {
898+
userDn = expandDnTemplate(userDnTemplate, matchedPrincipal);
899+
}
891900
LOGGER.debug("LDAP UserDN and Principal: {},{}", userDn, principal);
892901
return userDn;
893902
}
@@ -1045,8 +1054,8 @@ protected static final String expandFilterTemplate(final String template, final
10451054
* (e.g. {@code userDnTemplate}, {@code userSearchBase}). The {@code input}
10461055
* value is RFC 4514 escaped via the existing {@link #escapeAttributeValue}
10471056
* before substitution so that user-controlled DN metacharacters such as
1048-
* {@code ,}, {@code =}, {@code +} or leading {@code #} cannot break out of
1049-
* their RDN or inject additional RDNs.
1057+
* {@code ,}, {@code +} or leading {@code #} cannot break out of their RDN
1058+
* or inject additional RDNs.
10501059
*/
10511060
protected final String expandDnTemplate(final String template, final String input) {
10521061
String escaped = escapeAttributeValue(input);

zeppelin-server/src/test/java/org/apache/zeppelin/realm/ActiveDirectoryGroupRealmFilterInjectionTest.java

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.apache.zeppelin.realm;
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
20-
import static org.junit.jupiter.api.Assertions.assertFalse;
2120
import static org.junit.jupiter.api.Assertions.assertTrue;
2221
import static org.mockito.ArgumentMatchers.any;
2322
import static org.mockito.ArgumentMatchers.anyString;
@@ -79,30 +78,23 @@ void searchForUserName_escapes_user_payload(String payload) throws Exception {
7978
"admin)(cn=a*",
8079
")(mail=*@corp.com"
8180
})
82-
void doGetAuthorizationInfo_path_escapes_user_payload(String payload)
83-
throws Exception {
84-
// Drive getRoleNamesForUser indirectly via searchForUserName parameters
85-
// since getRoleNamesForUser is package-private; this exercises the same
86-
// String.format filter site at L304 with userPrincipalName.
81+
void getRoleNamesForUser_escapes_user_payload(String payload) throws Exception {
82+
// Exercise the second String.format filter site directly. The method is
83+
// private, so use reflection to invoke it.
8784
LdapContext ctx = mock(LdapContext.class);
8885
NamingEnumeration<SearchResult> empty = mock(NamingEnumeration.class);
8986
when(empty.hasMoreElements()).thenReturn(false);
9087
when(ctx.search(anyString(), anyString(), any(), any(SearchControls.class)))
9188
.thenReturn(empty);
9289

93-
ActiveDirectoryGroupRealm realm = new ActiveDirectoryGroupRealm() {
94-
// Expose the package-private method through a test-only hook.
95-
java.util.Set<String> testGetRoleNamesForUser(String username, LdapContext c)
96-
throws javax.naming.NamingException {
97-
// Mirror the call chain in queryForAuthorizationInfo
98-
return new java.util.LinkedHashSet<>();
99-
}
100-
};
90+
ActiveDirectoryGroupRealm realm = new ActiveDirectoryGroupRealm();
10191
realm.setSearchBase("dc=example,dc=com");
10292

103-
// Use searchForUserName as proxy — the same escape utility is applied at
104-
// both filter sites and the assertion holds for either one.
105-
realm.searchForUserName(payload, ctx, 100);
93+
java.lang.reflect.Method method =
94+
ActiveDirectoryGroupRealm.class.getDeclaredMethod(
95+
"getRoleNamesForUser", String.class, LdapContext.class);
96+
method.setAccessible(true);
97+
method.invoke(realm, payload, ctx);
10698

10799
ArgumentCaptor<String> filterCap = ArgumentCaptor.forClass(String.class);
108100
verify(ctx).search(anyString(), filterCap.capture(), any(),
Binary file not shown.

zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmFilterInjectionTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.apache.zeppelin.realm;
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
20-
import static org.junit.jupiter.api.Assertions.assertFalse;
2120
import static org.junit.jupiter.api.Assertions.assertTrue;
2221

2322
import org.junit.jupiter.api.Test;

zeppelin-server/src/test/java/org/apache/zeppelin/realm/LdapRealmTest.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ class LdapRealmTest {
4747
void testGetUserDn() {
4848
LdapRealm realm = new LdapRealm();
4949

50-
// without a user search filter — trailing space is RFC 4514-escaped (\\20)
51-
// because getUserDn expands via expandDnTemplate which calls escapeAttributeValue.
50+
// without a user search filter — when userDnTemplate is the bare
51+
// placeholder, the principal is treated as a full DN supplied verbatim
52+
// (no escape) so trailing-space inputs are preserved unchanged.
5253
realm.setUserSearchFilter(null);
53-
assertEquals("foo\\20", realm.getUserDn("foo "));
54+
assertEquals("foo ", realm.getUserDn("foo "));
5455

5556
// with a user search filter
5657
realm.setUserSearchFilter("memberUid={0}");
@@ -127,10 +128,13 @@ void testFilterEscaping() {
127128
assertEquals("foo\\2B", realm.escapeAttributeValue("foo+"));
128129
assertEquals("foo\\5C", realm.escapeAttributeValue("foo\\"));
129130
assertEquals("foo\\00", realm.escapeAttributeValue("foo\u0000"));
131+
// setUserSearchFilter / setGroupSearchFilter store the operator-supplied
132+
// template verbatim; user-controlled values are escaped at substitution
133+
// time by expandFilterTemplate, not at config time.
130134
realm.setUserSearchFilter("uid=<{0}>");
131-
assertEquals("uid=\\3C{0}\\3E", realm.getUserSearchFilter());
135+
assertEquals("uid=<{0}>", realm.getUserSearchFilter());
132136
realm.setUserSearchFilter("gid=\\{0}\\");
133-
assertEquals("gid=\\5C{0}\\5C", realm.getUserSearchFilter());
137+
assertEquals("gid=\\{0}\\", realm.getUserSearchFilter());
134138
}
135139

136140
private NamingEnumeration<SearchResult> enumerationOf(BasicAttributes... attrs) {

0 commit comments

Comments
 (0)