Skip to content

Commit 2714791

Browse files
authored
Fix ACI grouped bind rule wrongly rejected when a value contains parentheses (#716)
1 parent dc20037 commit 2714791

2 files changed

Lines changed: 116 additions & 9 deletions

File tree

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2008 Sun Microsystems, Inc.
1515
* Portions Copyright 2013-2016 ForgeRock AS.
16+
* Portions Copyright 2026 3A Systems, LLC
1617
*/
1718
package org.opends.server.authorization.dseecompat;
1819

@@ -133,11 +134,10 @@ private BindRule(BindRule left, BindRule right, EnumBooleanTypes booleanType) {
133134
}
134135

135136
/*
136-
* TODO Verify this method handles escaped parentheses by writing
137-
* a unit test.
138-
*
139-
* It doesn't look like the decode() method handles the possibility of
140-
* escaped parentheses in a bind rule.
137+
* Parentheses embedded in a quoted bind rule expression (for example a DN
138+
* value such as userdn="ldap:///cn=a(b),dc=example,dc=com") are treated as
139+
* literal data and are not mistaken for grouping parentheses. See the
140+
* quote-aware scan below and BindRuleParenTest for the covering cases.
141141
*/
142142
/**
143143
* Decode an ACI bind rule string representation.
@@ -160,19 +160,27 @@ public static BindRule decode (String input) throws AciException {
160160
int currentPos;
161161
int numOpen = 0;
162162
int numClose = 0;
163+
boolean inQuotes = false;
163164

164-
// Find the associated closed parenthesis
165+
// Find the associated closed parenthesis. Parentheses that appear
166+
// inside a quoted bind rule expression (e.g. a DN value containing
167+
// '(' or ')') are literal data and must not be counted as grouping.
165168
for (currentPos = 0; currentPos < bindruleArray.length; currentPos++)
166169
{
167-
if (bindruleArray[currentPos] == '(')
170+
char currentChar = bindruleArray[currentPos];
171+
if (currentChar == '"')
172+
{
173+
inQuotes = !inQuotes;
174+
}
175+
else if (!inQuotes && currentChar == '(')
168176
{
169177
numOpen++;
170178
}
171-
else if (bindruleArray[currentPos] == ')')
179+
else if (!inQuotes && currentChar == ')')
172180
{
173181
numClose++;
174182
}
175-
if (numClose == numOpen)
183+
if (!inQuotes && numClose == numOpen)
176184
{
177185
// We found the associated closed parenthesis the parenthesis are removed
178186
String bindruleStr1 = bindruleStr.substring(1, currentPos);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions Copyright [year] [name of copyright owner]".
13+
*
14+
* Portions Copyright 2026 3A Systems, LLC
15+
*/
16+
package org.opends.server.authorization.dseecompat;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.opends.server.DirectoryServerTestCase;
21+
import org.opends.server.TestCaseUtils;
22+
import org.opends.server.types.DirectoryException;
23+
import org.testng.annotations.AfterClass;
24+
import org.testng.annotations.BeforeClass;
25+
import org.testng.annotations.DataProvider;
26+
import org.testng.annotations.Test;
27+
28+
/**
29+
* Verifies that {@link BindRule#decode(String)} treats parentheses embedded in a
30+
* quoted bind rule expression (e.g. a DN value containing '(' or ')') as literal
31+
* data instead of grouping parentheses. Before the quote-aware scan, a grouped
32+
* bind rule whose value contained a parenthesis was wrongly rejected because the
33+
* embedded parenthesis was counted when locating the matching close parenthesis.
34+
* Also verifies the scan stays fail-closed: genuinely unbalanced grouping
35+
* parentheses (outside quotes) are still rejected with an {@link AciException}.
36+
*/
37+
@SuppressWarnings("javadoc")
38+
public class BindRuleParenTest extends DirectoryServerTestCase
39+
{
40+
@BeforeClass
41+
public void setUp() throws Exception
42+
{
43+
TestCaseUtils.startFakeServer();
44+
}
45+
46+
@AfterClass
47+
public void tearDown() throws DirectoryException
48+
{
49+
TestCaseUtils.shutdownFakeServer();
50+
}
51+
52+
@DataProvider(name = "acisWithParenInValue")
53+
public Object[][] acisWithParenInValue()
54+
{
55+
return new Object[][] {
56+
// simple (ungrouped) bind rule with a ')' / '(' in the DN value
57+
{ "(version 3.0; acl \"t\"; allow (search) userdn=\"ldap:///cn=a)b,dc=x\";)" },
58+
{ "(version 3.0; acl \"t\"; allow (search) userdn=\"ldap:///cn=a(b,dc=x\";)" },
59+
// grouped bind rule with a ')' embedded in a value
60+
{ "(version 3.0; acl \"t\"; allow (search) "
61+
+ "(userdn=\"ldap:///cn=a)b,dc=x\" or userdn=\"ldap:///self\");)" },
62+
// grouped bind rule with a '(' embedded in a value
63+
{ "(version 3.0; acl \"t\"; allow (search) "
64+
+ "(userdn=\"ldap:///cn=a(b,dc=x\" or userdn=\"ldap:///self\");)" },
65+
// grouped bind rule with matched parentheses embedded in a value
66+
{ "(version 3.0; acl \"t\"; allow (search) "
67+
+ "(userdn=\"ldap:///cn=a(b),dc=x\" or userdn=\"ldap:///self\");)" },
68+
};
69+
}
70+
71+
@Test(dataProvider = "acisWithParenInValue")
72+
public void decodesParenInsideQuotedValue(String aci) throws Exception
73+
{
74+
AciBody body = AciBody.decode(aci);
75+
assertThat(body.getPermBindRulePairs()).hasSize(1);
76+
assertThat(body.getPermBindRulePairs().get(0).getBindRule()).isNotNull();
77+
}
78+
79+
@DataProvider(name = "malformedBindRules")
80+
public Object[][] malformedBindRules()
81+
{
82+
return new Object[][] {
83+
// grouped bind rule with no close parenthesis at all
84+
{ "(userdn=\"ldap:///self\"" },
85+
// grouped bind rule with one extra (unmatched) open parenthesis
86+
{ "((userdn=\"ldap:///self\")" },
87+
// grouped bind rule whose only ')' is embedded in a quoted value, so the
88+
// group is genuinely unterminated: the quote-aware scan must not treat the
89+
// embedded ')' as the closing parenthesis and silently accept the input.
90+
{ "(userdn=\"ldap:///cn=a)b,dc=x\" or userdn=\"ldap:///self\"" },
91+
};
92+
}
93+
94+
@Test(dataProvider = "malformedBindRules")
95+
public void rejectsUnbalancedParensOutsideQuotes(String bindRule)
96+
{
97+
assertThatThrownBy(() -> BindRule.decode(bindRule)).isInstanceOf(AciException.class);
98+
}
99+
}

0 commit comments

Comments
 (0)