Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Fix audit log writing errors for rollover-enabled alias indices ([#5878](https://github.com/opensearch-project/security/pull/5878)

Comment thread
cwperks marked this conversation as resolved.
Resolved
- Fix the issue of unprocessed X-Request-Id ([#5954](https://github.com/opensearch-project/security/pull/5954))
- Improve DLS error message to identify undefined user attributes when query substitution fails ([#5975](https://github.com/opensearch-project/security/pull/5975))
- Fix the issue of unprocessed X-Request-Id ([#5976](https://github.com/opensearch-project/security/pull/5976))
Comment thread
ThyTran1402 marked this conversation as resolved.
Resolved
### Refactoring

### Maintenance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import org.opensearch.test.framework.TestSecurityConfig;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.opensearch.security.util.MockIndexMetadataBuilder.dataStreams;
import static org.opensearch.security.util.MockIndexMetadataBuilder.indices;
import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -1175,6 +1176,33 @@ public void invalidTemplatedQuery() throws Exception {
.evaluate(MockPrivilegeEvaluationContextBuilder.ctx().get());
}

@Test
public void invalidTemplatedQuery_errorMessageIdentifiesUndefinedAttribute() throws Exception {
try {
DocumentPrivileges.DlsQuery.create("{\"terms\":{\"arr\":[${attr.jwt.array}]}}", xContentRegistry)
.evaluate(MockPrivilegeEvaluationContextBuilder.ctx().attr("attr.jwt.other", "x").get());
fail("Expected PrivilegesEvaluationException");
} catch (PrivilegesEvaluationException e) {
assertThat(e.getMessage(), containsString("attr.jwt.array"));
assertThat(e.getMessage(), containsString("attr.jwt.other"));
}
}

@Test
public void invalidTemplatedQuery_errorMessageTruncatesWhenMoreThanTenAvailableAttributes() throws Exception {
MockPrivilegeEvaluationContextBuilder ctx = MockPrivilegeEvaluationContextBuilder.ctx();
for (int i = 1; i <= 12; i++) {
ctx.attr("attr.jwt.attr" + i, "v" + i);
}
try {
DocumentPrivileges.DlsQuery.create("{\"term\":{\"dept\":\"${attr.jwt.missing}\"}}", xContentRegistry).evaluate(ctx.get());
fail("Expected PrivilegesEvaluationException");
} catch (PrivilegesEvaluationException e) {
assertThat(e.getMessage(), containsString("attr.jwt.missing"));
assertThat(e.getMessage(), containsString("... and 2 more"));
}
}

@Test
public void equals() throws Exception {
DocumentPrivileges.DlsQuery query1a = DocumentPrivileges.DlsQuery.create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
*/
package org.opensearch.security.privileges;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
Expand All @@ -25,10 +30,25 @@
* This code was moved over from ConfigModelV7.
*/
public class UserAttributes {
private static final Pattern UNRESOLVED_ATTRIBUTE_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");

public static boolean needsAttributeSubstitution(String patternString) {
return patternString.contains("${");
}

/**
* Returns the names of all unresolved ${...} attribute references remaining
* in {@code s} after substitution has been performed.
*/
public static List<String> findUnresolvedAttributes(String s) {
List<String> result = new ArrayList<>();
Matcher matcher = UNRESOLVED_ATTRIBUTE_PATTERN.matcher(s);
while (matcher.find()) {

@rursprung rursprung Apr 23, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole method can be simplified to return UNRESOLVED_ATTRIBUTE_PATTERN.matcher(s).results().map(m -> m.group(1)).toList();
see Matcher#results

(stumbled over it by chance while reviewing a bug)

@ThyTran1402 ThyTran1402 Apr 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm that's interesting. It seems to be cleaner with Matcher#results().
I wonder should I open another PR for this suggestion since it already merged?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i raised #6122

result.add(matcher.group(1));
}
return Collections.unmodifiableList(result);
}

public static String replaceProperties(String orig, PrivilegesEvaluationContext context) {
User user = context.getUser();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.logging.log4j.util.Strings;

Expand Down Expand Up @@ -46,6 +48,8 @@
* Instances of this class are managed by DlsFlsProcessedConfig.
*/
public class DocumentPrivileges extends AbstractRuleBasedPrivileges<DocumentPrivileges.DlsQuery, DlsRestriction> {
private static final int MAX_ATTRIBUTES_IN_ERROR_MESSAGE = 10;

private final NamedXContentRegistry xContentRegistry;

public DocumentPrivileges(
Expand Down Expand Up @@ -176,8 +180,16 @@ static class Dynamic extends DlsQuery {
RenderedDlsQuery evaluate(PrivilegesEvaluationContext context) throws PrivilegesEvaluationException {
String effectiveQueryString = UserAttributes.replaceProperties(this.queryString, context);
if (UserAttributes.needsAttributeSubstitution(effectiveQueryString)) {
List<String> unresolved = UserAttributes.findUnresolvedAttributes(effectiveQueryString);
Set<String> available = context.getUser().getCustomAttributesMap().keySet();
String availableStr = available.size() > MAX_ATTRIBUTES_IN_ERROR_MESSAGE
? available.stream().limit(MAX_ATTRIBUTES_IN_ERROR_MESSAGE).collect(Collectors.joining(", "))
+ " ... and "
+ (available.size() - MAX_ATTRIBUTES_IN_ERROR_MESSAGE)
+ " more"
: String.join(", ", available);
throw new PrivilegesEvaluationException(
"Invalid DLS query: " + effectiveQueryString,
"DLS query references undefined user attributes: " + unresolved + ". Available attributes are: " + availableStr,
new OpenSearchSecurityException("User attribute substitution failed")
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
public class UserAttributesUnitTest {
@Test
public void testNeedsAttributeSubstitution() {
assertTrue(UserAttributes.needsAttributeSubstitution("{\"foo\": \"${user.name}}\""));
assertTrue(UserAttributes.needsAttributeSubstitution("""
{"foo": "${user.name}}"}"""));
assertTrue(UserAttributes.needsAttributeSubstitution("${attr1.proxy.foo}"));
assertFalse(UserAttributes.needsAttributeSubstitution("{\"foo\": \"bar\"}"));
assertFalse(UserAttributes.needsAttributeSubstitution("""
{"foo": "bar"}"""));
}

@Test
Expand Down Expand Up @@ -71,4 +73,34 @@ public void testReplaceProperties() {
""";
assertEquals(expectedString, UserAttributes.replaceProperties(stringWithPlaceholders, ctx));
}

@Test
public void testFindUnresolvedAttributes_noTokens() {
assertTrue(UserAttributes.findUnresolvedAttributes("""
{"term": {"dept": "value"}}""").isEmpty());
}

@Test
public void testFindUnresolvedAttributes_singleToken() {
assertEquals(List.of("attr.jwt.array"), UserAttributes.findUnresolvedAttributes("""
{"terms": {"arr": [${attr.jwt.array}]}}"""));
}

@Test
public void testFindUnresolvedAttributes_multipleTokens() {
assertEquals(List.of("attr.jwt.dept", "attr.proxy.region"), UserAttributes.findUnresolvedAttributes("""
{
"bool": {
"must": [
{"term": {"dept": "${attr.jwt.dept}"}},
{"term": {"region": "${attr.proxy.region}"}}
]
}
}"""));
}

@Test(expected = UnsupportedOperationException.class)
public void testFindUnresolvedAttributes_returnsUnmodifiableList() {
UserAttributes.findUnresolvedAttributes("${attr.foo}").add("extra");
}
}
Loading