Skip to content

DLS for pluggable data formats and query execution - #6569

Draft
nibix wants to merge 1 commit into
opensearch-project:mainfrom
nibix:pluggable-df-dls
Draft

nibix wants to merge 1 commit into
opensearch-project:mainfrom
nibix:pluggable-df-dls

Conversation

@nibix

@nibix nibix commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds logical-plan DLS enforcement for analytics queries according to RFC spec in #22756 .

This implements the security plugin side of the core PR opensearch-project/OpenSearch#23168 .

It mostly boils down to exposing DLS rules via the AccessPolicyProviderPlugin interface.

Testing

  • Unit tests cover unrestricted, fully restricted (match_none), combined role queries, grouped restrictions, and overlapping-group rejection.
  • Security integration tests install a mock logical-read consumer that obtains policies through ReadAccessPolicyService. They verify the actual Security provider returns the expected restriction for a DLS user and no restriction for an unrestricted user.
  • This complements the OpenSearch-side analytics QA test, which uses a mock AccessPolicyProviderPlugin to validate analytics enforcement independently of the Security plugin.

Check List

  • New functionality includes testing
  • Commits are signed per the DCO using --signoff

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Signed-off-by: Nils Bandener <nils.bandener@eliatra.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Grouping by QueryBuilder identity

indicesByRestriction uses QueryBuilder as the map key, relying on equals/hashCode to group indices sharing the same restriction. However, DocumentPrivileges.RenderedDlsQuery typically produces distinct QueryBuilder instances per index (rendered per-index with different parameters/user context). If two indices have identical rendered queries but the resulting QueryBuilder instances do not implement value equality consistently for all query types, indices with equivalent restrictions may be placed into separate groups. Consider grouping by the rendered query string or another canonical representation, or verify equals/hashCode is reliable across all supported DLS query types.

Map<QueryBuilder, Set<String>> indicesByRestriction = new LinkedHashMap<>();
Set<String> unrestrictedIndices = new LinkedHashSet<>();
for (String index : context.concreteIndices()) {
    DlsRestriction restriction = baseContext.config().getDocumentPrivileges().getRestriction(privilegesContext, index);
    if (restriction.isUnrestricted()) {
        unrestrictedIndices.add(index);
    } else {
        QueryBuilder query = combine(restriction.getQueries());
        indicesByRestriction.computeIfAbsent(query, ignored -> new LinkedHashSet<>()).add(index);
    }
}

@github-actions

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Group indices by stable restriction key

Using a QueryBuilder as a Map key relies on equals/hashCode, which many QueryBuilder
subclasses (e.g., BoolQueryBuilder built from multiple role queries) do implement,
but the combine method creates a new BoolQueryBuilder per index even for identical
inputs. Two indices with the same set of role queries will produce two
BoolQueryBuilder instances that must be equal to be grouped — verify equality holds,
or group by the underlying List (or its rendered-string form) before combining to
guarantee grouping works as tested.

src/main/java/org/opensearch/security/privileges/dlsfls/ReadAccessPolicyProviderImpl.java [46-56]

-Map<QueryBuilder, Set<String>> indicesByRestriction = new LinkedHashMap<>();
+Map<List<DocumentPrivileges.RenderedDlsQuery>, Set<String>> indicesByRestrictionKey = new LinkedHashMap<>();
+Map<List<DocumentPrivileges.RenderedDlsQuery>, QueryBuilder> combinedByKey = new LinkedHashMap<>();
 Set<String> unrestrictedIndices = new LinkedHashSet<>();
 for (String index : context.concreteIndices()) {
     DlsRestriction restriction = baseContext.config().getDocumentPrivileges().getRestriction(privilegesContext, index);
     if (restriction.isUnrestricted()) {
         unrestrictedIndices.add(index);
     } else {
-        QueryBuilder query = combine(restriction.getQueries());
-        indicesByRestriction.computeIfAbsent(query, ignored -> new LinkedHashSet<>()).add(index);
+        List<DocumentPrivileges.RenderedDlsQuery> key = restriction.getQueries();
+        combinedByKey.computeIfAbsent(key, k -> combine(k));
+        indicesByRestrictionKey.computeIfAbsent(key, ignored -> new LinkedHashSet<>()).add(index);
     }
 }
+Map<QueryBuilder, Set<String>> indicesByRestriction = new LinkedHashMap<>();
+indicesByRestrictionKey.forEach((k, v) -> indicesByRestriction.put(combinedByKey.get(k), v));
Suggestion importance[1-10]: 6

__

Why: The concern is valid: using freshly-constructed BoolQueryBuilder instances as map keys depends on equals/hashCode being properly implemented across subclasses. The test groupsIndicesWithEqualRestrictions passes only because QueryBuilder.equals is implemented, but the suggestion improves robustness by using a more explicit grouping key.

Low
Mirror super call in serialization

The writeTo method does not call super.writeTo(out), but the matching constructor
calls super(in). While ActionResponse.writeTo is typically a no-op, the asymmetry is
inconsistent with the request class in the same file which correctly calls
super.writeTo(out). Add the super call for symmetry and future-proofing.

src/integrationTest/java/org/opensearch/security/privileges/int_tests/CompositeIndexAuthorizationIntTests.java [221-223]

 public MockReadAccessResponse(StreamInput in) throws IOException {
     super(in);
     restrictions = in.readMap(StreamInput::readString, StreamInput::readString);
 }
 
 @Override
 public void writeTo(StreamOutput out) throws IOException {
+    super.writeTo(out);
     out.writeMap(restrictions, StreamOutput::writeString, StreamOutput::writeString);
 }
Suggestion importance[1-10]: 3

__

Why: Minor consistency improvement in test code; ActionResponse.writeTo is typically a no-op, so the practical impact is low.

Low

@nibix

nibix commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

@cwperks @DarshitChanpura @Bukhtawar @niravpi

Please have a look at this.

It should already be the complete implementation. I left it in draft state, because we have to coordinate merging on the core and security side (at the moment compilation fails because it is missing the new interfaces introduced in core).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant