Skip to content

Commit 6d47862

Browse files
committed
Address review comments on #719 bind-rule operand fix
- Hoist the null left-operand guard into BindRule.decode, above the remaining-chars test, so one check rejects both a bare empty group ("()") and a missing left operand ("()or userdn=..."), and the message names the full offending input. Remove the now-redundant null guard in createBindRule (its only null-capable caller is guarded upstream). - Correct the bindrule_2 comment: "(()or)" is caught by the left guard, so use the input that actually reaches it ('userdn="ldap:///self" or'). - Document decode's null base case in the javadoc and fix the stale blank-bind-rule comment. - Add tests: malformed "(())", "( )", '... or ()', "not ()"; positive controls for valid nested/complex rules; and an ACI-level missing-left regression case.
1 parent 93bcfd0 commit 6d47862

2 files changed

Lines changed: 61 additions & 25 deletions

File tree

opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/BindRule.java

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ private BindRule(BindRule left, BindRule right, EnumBooleanTypes booleanType) {
142142
/**
143143
* Decode an ACI bind rule string representation.
144144
* @param input The string representation of the bind rule.
145-
* @return A BindRule class representing the bind rule.
145+
* @return A BindRule class representing the bind rule, or {@code null} if
146+
* {@code input} is null or blank. This null is the recursion base case for
147+
* an empty group; callers must reject it rather than dereference it.
146148
* @throws AciException If the string is an invalid bind rule.
147149
*/
148150
public static BindRule decode (String input) throws AciException {
@@ -153,8 +155,10 @@ public static BindRule decode (String input) throws AciException {
153155
String bindruleStr = input.trim();
154156
if (bindruleStr.isEmpty())
155157
{
156-
// A blank bind rule (e.g. "( )") must be rejected as a
157-
// syntax error rather than throwing StringIndexOutOfBoundsException.
158+
// A blank bind rule (e.g. "( )") decodes to null rather than
159+
// throwing StringIndexOutOfBoundsException on charAt(0). This null is
160+
// the recursion base case for an empty group; the caller rejects it as
161+
// a syntax error (see the bindrule_1 == null check below).
158162
return null;
159163
}
160164
char firstChar = bindruleStr.charAt(0);
@@ -202,6 +206,17 @@ else if (!inQuotes && currentChar == ')')
202206
if (numOpen > numClose) {
203207
throw new AciException(WARN_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input));
204208
}
209+
/*
210+
* An empty group such as "()" or "( )" decodes to a null bind rule.
211+
* Such a group is not a valid bind rule on its own, and using it as the
212+
* left operand of an "and"/"or" (e.g. "()or userdn=...") would build a
213+
* complex rule with a null left side that is later dereferenced during
214+
* evaluation. Reject both cases here, before the remaining-chars test,
215+
* so the message names the full offending input rather than a fragment.
216+
*/
217+
if (bindrule_1 == null) {
218+
throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input));
219+
}
205220
/*
206221
* If there are remaining chars => there MUST be an operand (AND / OR)
207222
* otherwise there is a syntax error
@@ -212,14 +227,6 @@ else if (!inQuotes && currentChar == ')')
212227
bindruleStr.substring(currentPos + 1);
213228
return createBindRule(bindrule_1, remainingBindruleStr);
214229
}
215-
/*
216-
* An empty group such as "()" decodes to a null bind rule. A group
217-
* with no bind rule inside is not valid, so reject it instead of
218-
* returning null and letting the caller dereference it later.
219-
*/
220-
if (bindrule_1 == null) {
221-
throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input));
222-
}
223230
return bindrule_1;
224231
}
225232
else
@@ -303,15 +310,6 @@ private static BindRule parseAndCreateBindrule(Matcher bindruleMatcher) throws A
303310
*/
304311
private static BindRule createBindRule(BindRule bindrule,
305312
String remainingBindruleStr) throws AciException {
306-
/*
307-
* The left operand of an "and"/"or" bind rule must exist. It is null when
308-
* the left side reduced to an empty group such as "()" (for example the
309-
* bind rule "()or userdn=..."). A complex bind rule with a null left side
310-
* would later be dereferenced during evaluation, so reject it here.
311-
*/
312-
if (bindrule == null) {
313-
throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr));
314-
}
315313
Pattern remainingBindrulePattern = Pattern.compile(remainingBindruleRegex);
316314
Matcher remainingBindruleMatcher = remainingBindrulePattern.matcher(remainingBindruleStr);
317315
if (remainingBindruleMatcher.find()) {
@@ -338,7 +336,8 @@ private static BindRule createBindRule(BindRule bindrule,
338336
/*
339337
* The right operand of an "and"/"or" bind rule must exist. It is null
340338
* when the boolean operator is not followed by a bind rule, e.g. the
341-
* "or" in "(()or)" has nothing to its right (issue #719).
339+
* "or" in "userdn=\"ldap:///self\" or" has nothing to its right
340+
* (issue #719).
342341
*/
343342
if (bindrule_2 == null) {
344343
throw new AciException(

opendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/BindRuleOperandTest.java

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ public Object[][] bindRulesWithMissingOperand()
6565
{ "()or userdn=\"ldap:///self\"" },
6666
// an empty group is not a bind rule on its own
6767
{ "()" },
68+
// a nested empty group is still empty
69+
{ "(())" },
70+
// a whitespace-only group is empty once trimmed
71+
{ "( )" },
72+
// empty right *group*: reaches the decode-level guard from createBindRule
73+
{ "(userdn=\"ldap:///self\" or ())" },
74+
// "not" applied to an empty group
75+
{ "not ()" },
6876
};
6977
}
7078

@@ -74,11 +82,40 @@ public void rejectsMissingOperand(String bindRule)
7482
assertThatThrownBy(() -> BindRule.decode(bindRule)).isInstanceOf(AciException.class);
7583
}
7684

77-
/** Reproduces the full ACI from issue #719 through {@link Aci#decode}. */
78-
@Test
79-
public void decodeOfAciWithMissingOperandThrowsAciException()
85+
@DataProvider(name = "validBindRules")
86+
public Object[][] validBindRules()
87+
{
88+
return new Object[][] {
89+
// a valid rule inside a nested group must still decode: guards must not
90+
// start over-rejecting non-empty groups
91+
{ "((userdn=\"ldap:///self\"))" },
92+
// a valid complex rule with both operands present
93+
{ "(userdn=\"ldap:///self\" or userdn=\"ldap:///anyone\")" },
94+
};
95+
}
96+
97+
@Test(dataProvider = "validBindRules")
98+
public void acceptsValidBindRule(String bindRule) throws Exception
99+
{
100+
assertThat(BindRule.decode(bindRule)).isNotNull();
101+
}
102+
103+
@DataProvider(name = "acisWithMissingOperand")
104+
public Object[][] acisWithMissingOperand()
105+
{
106+
return new Object[][] {
107+
// the full ACI from issue #719
108+
{ "(version 3.0; acl \"ac\"; allow (search)(()or) (userdn=\"ldap:///self\"); )" },
109+
// missing *left* operand: previously accepted and stored with left == null,
110+
// then NPEd during evaluation. Must now be rejected at decode time.
111+
{ "(version 3.0; acl \"ac\"; allow (search) ()or userdn=\"ldap:///self\"; )" },
112+
};
113+
}
114+
115+
/** Reproduces missing-operand ACIs from issue #719 through {@link Aci#decode}. */
116+
@Test(dataProvider = "acisWithMissingOperand")
117+
public void decodeOfAciWithMissingOperandThrowsAciException(String aci)
80118
{
81-
String aci = "(version 3.0; acl \"ac\"; allow (search)(()or) (userdn=\"ldap:///self\"); )";
82119
assertThatThrownBy(() -> Aci.decode(ByteString.valueOfUtf8(aci), DN.rootDN()))
83120
.isInstanceOf(AciException.class);
84121
}

0 commit comments

Comments
 (0)