1414import org .elasticsearch .common .bytes .BytesReference ;
1515import org .elasticsearch .common .io .stream .StreamOutput ;
1616import org .elasticsearch .common .lucene .search .Queries ;
17+ import org .elasticsearch .core .Nullable ;
1718import org .elasticsearch .index .mapper .NestedLookup ;
1819import org .elasticsearch .index .query .QueryBuilder ;
1920import org .elasticsearch .index .query .QueryRewriteContext ;
3637import java .util .SortedSet ;
3738import java .util .TreeSet ;
3839import java .util .function .Function ;
40+ import java .util .stream .Stream ;
3941
4042import static org .apache .lucene .search .BooleanClause .Occur .FILTER ;
4143import static org .apache .lucene .search .BooleanClause .Occur .SHOULD ;
4648 * queries are used as an additional filter.
4749 */
4850public final class DocumentPermissions implements CacheKey {
49- // SortedSet because orders are important when they get serialised for request cache key
50- private final SortedSet <BytesReference > queries ;
51- private final SortedSet <BytesReference > limitedByQueries ;
52- private List <String > evaluatedQueries ;
53- private List <String > evaluatedLimitedByQueries ;
5451
55- private static DocumentPermissions ALLOW_ALL = new DocumentPermissions ();
52+ @ Nullable
53+ private final List <Set <BytesReference >> listOfQueries ;
54+ @ Nullable
55+ private List <List <String >> listOfEvaluatedQueries ;
5656
57- DocumentPermissions () {
58- this .queries = null ;
59- this .limitedByQueries = null ;
57+ private static final DocumentPermissions ALLOW_ALL = new DocumentPermissions ();
58+
59+ private DocumentPermissions () {
60+ this .listOfQueries = null ;
6061 }
6162
62- DocumentPermissions (Set <BytesReference > queries ) {
63- this (queries , null );
63+ private DocumentPermissions (Set <BytesReference > queries ) {
64+ assert queries != null && false == queries .isEmpty () : "null or empty queries not permitted" ;
65+ this .listOfQueries = List .of (new TreeSet <>(queries ));
6466 }
6567
66- DocumentPermissions (Set <BytesReference > queries , Set <BytesReference > scopedByQueries ) {
67- if (queries == null && scopedByQueries == null ) {
68- throw new IllegalArgumentException ("one of the queries or scoped queries must be provided" );
69- }
70- this .queries = (queries != null ) ? new TreeSet <>(queries ) : null ;
71- this .limitedByQueries = (scopedByQueries != null ) ? new TreeSet <>(scopedByQueries ) : null ;
68+ private DocumentPermissions (List <Set <BytesReference >> listOfQueries ) {
69+ assert listOfQueries != null && false == listOfQueries .isEmpty () : "null or empty list of queries not permitted" ;
70+ assert listOfQueries .stream ().allMatch (queries -> queries != null && false == queries .isEmpty ())
71+ : "null or empty queries not permitted" ;
72+ // SortedSet because orders are important when they get serialised for request cache key
73+ this .listOfQueries = listOfQueries .stream ()
74+ .map (queries -> queries instanceof SortedSet <BytesReference > ? queries : new TreeSet <>(queries ))
75+ .toList ();
7276 }
7377
74- public Set <BytesReference > getQueries () {
75- return queries == null ? null : Set . copyOf ( queries ) ;
78+ public List < Set <BytesReference >> getListOfQueries () {
79+ return listOfQueries ;
7680 }
7781
78- public Set <BytesReference > getLimitedByQueries () {
79- return limitedByQueries == null ? null : Set .copyOf (limitedByQueries );
82+ public Set <BytesReference > getSingleSetOfQueries () {
83+ assert listOfQueries != null && listOfQueries .size () == 1 : "the list of queries does not have a single member" ;
84+ return listOfQueries .get (0 );
8085 }
8186
8287 /**
8388 * @return {@code true} if either queries or scoped queries are present for document level security else returns {@code false}
8489 */
8590 public boolean hasDocumentLevelPermissions () {
86- return queries != null || limitedByQueries != null ;
91+ return listOfQueries != null ;
8792 }
8893
8994 public boolean hasStoredScript () throws IOException {
90- if (queries != null ) {
91- for (BytesReference q : queries ) {
92- if (DLSRoleQueryValidator .hasStoredScript (q , NamedXContentRegistry .EMPTY )) {
93- return true ;
94- }
95- }
96- }
97- if (limitedByQueries != null ) {
98- for (BytesReference q : limitedByQueries ) {
99- if (DLSRoleQueryValidator .hasStoredScript (q , NamedXContentRegistry .EMPTY )) {
100- return true ;
95+ if (listOfQueries != null ) {
96+ for (Set <BytesReference > queries : listOfQueries ) {
97+ for (BytesReference q : queries ) {
98+ if (DLSRoleQueryValidator .hasStoredScript (q , NamedXContentRegistry .EMPTY )) {
99+ return true ;
100+ }
101101 }
102102 }
103103 }
@@ -125,35 +125,25 @@ public BooleanQuery filter(
125125 ) throws IOException {
126126 if (hasDocumentLevelPermissions ()) {
127127 evaluateQueries (SecurityQueryTemplateEvaluator .wrap (user , scriptService ));
128- BooleanQuery .Builder filter ;
129- if (evaluatedQueries != null && evaluatedLimitedByQueries != null ) {
130- filter = new BooleanQuery .Builder ();
131- BooleanQuery .Builder scopedFilter = new BooleanQuery .Builder ();
132- buildRoleQuery (shardId , searchExecutionContextProvider , evaluatedLimitedByQueries , scopedFilter );
133- filter .add (scopedFilter .build (), FILTER );
128+ assert listOfEvaluatedQueries != null : "evaluated queries must not be null" ;
129+ assert false == listOfEvaluatedQueries .isEmpty () : "evaluated queries must not be empty" ;
134130
135- buildRoleQuery (shardId , searchExecutionContextProvider , evaluatedQueries , filter );
136- } else if (evaluatedQueries != null ) {
137- filter = new BooleanQuery .Builder ();
138- buildRoleQuery (shardId , searchExecutionContextProvider , evaluatedQueries , filter );
139- } else if (evaluatedLimitedByQueries != null ) {
140- filter = new BooleanQuery .Builder ();
141- buildRoleQuery (shardId , searchExecutionContextProvider , evaluatedLimitedByQueries , filter );
142- } else {
143- assert false : "one of queries and limited-by queries must be non-null" ;
144- return null ;
131+ BooleanQuery .Builder filter = new BooleanQuery .Builder ();
132+ for (int i = listOfEvaluatedQueries .size () - 1 ; i > 0 ; i --) {
133+ final BooleanQuery .Builder scopedFilter = new BooleanQuery .Builder ();
134+ buildRoleQuery (shardId , searchExecutionContextProvider , listOfEvaluatedQueries .get (i ), scopedFilter );
135+ filter .add (scopedFilter .build (), FILTER );
145136 }
137+ // TODO: All role queries can be filters
138+ buildRoleQuery (shardId , searchExecutionContextProvider , listOfEvaluatedQueries .get (0 ), filter );
146139 return filter .build ();
147140 }
148141 return null ;
149142 }
150143
151144 private void evaluateQueries (DlsQueryEvaluationContext context ) {
152- if (queries != null && evaluatedQueries == null ) {
153- evaluatedQueries = queries .stream ().map (context ::evaluate ).toList ();
154- }
155- if (limitedByQueries != null && evaluatedLimitedByQueries == null ) {
156- evaluatedLimitedByQueries = limitedByQueries .stream ().map (context ::evaluate ).toList ();
145+ if (listOfQueries != null && listOfEvaluatedQueries == null ) {
146+ listOfEvaluatedQueries = listOfQueries .stream ().map (queries -> queries .stream ().map (context ::evaluate ).toList ()).toList ();
157147 }
158148 }
159149
@@ -216,18 +206,9 @@ static void failIfQueryUsesClient(QueryBuilder queryBuilder, QueryRewriteContext
216206 * @return {@link DocumentPermissions}
217207 */
218208 public static DocumentPermissions filteredBy (Set <BytesReference > queries ) {
219- if (queries == null || queries .isEmpty ()) {
220- throw new IllegalArgumentException ("null or empty queries not permitted" );
221- }
222209 return new DocumentPermissions (queries );
223210 }
224211
225- /**
226- * Create {@link DocumentPermissions} with no restriction. The {@link #getQueries()}
227- * will return {@code null} in this case and {@link #hasDocumentLevelPermissions()}
228- * will be {@code false}
229- * @return {@link DocumentPermissions}
230- */
231212 public static DocumentPermissions allowAll () {
232213 return ALLOW_ALL ;
233214 }
@@ -240,49 +221,41 @@ public static DocumentPermissions allowAll() {
240221 * @return instance of {@link DocumentPermissions}
241222 */
242223 public DocumentPermissions limitDocumentPermissions (DocumentPermissions limitedByDocumentPermissions ) {
243- assert limitedByQueries == null && limitedByDocumentPermissions .limitedByQueries == null
244- : "nested scoping for document permissions is not permitted" ;
245- if (queries == null && limitedByDocumentPermissions .queries == null ) {
224+ if (hasDocumentLevelPermissions () && limitedByDocumentPermissions .hasDocumentLevelPermissions ()) {
225+ return new DocumentPermissions (
226+ Stream .concat (getListOfQueries ().stream (), limitedByDocumentPermissions .getListOfQueries ().stream ()).toList ()
227+ );
228+ } else if (hasDocumentLevelPermissions ()) {
229+ return new DocumentPermissions (getListOfQueries ());
230+ } else if (limitedByDocumentPermissions .hasDocumentLevelPermissions ()) {
231+ return new DocumentPermissions (limitedByDocumentPermissions .getListOfQueries ());
232+ } else {
246233 return DocumentPermissions .allowAll ();
247234 }
248- // TODO: should we apply the same logic here as FieldPermissions#limitFieldPermissions,
249- // i.e. treat limited-by as queries if original queries is null?
250- return new DocumentPermissions (queries , limitedByDocumentPermissions .queries );
251235 }
252236
253237 @ Override
254238 public String toString () {
255- return "DocumentPermissions [queries =" + queries + ", scopedByQueries=" + limitedByQueries + "]" ;
239+ return "DocumentPermissions [listOfQueries =" + listOfQueries + "]" ;
256240 }
257241
258242 @ Override
259243 public void buildCacheKey (StreamOutput out , DlsQueryEvaluationContext context ) throws IOException {
260- assert false == ( queries == null && limitedByQueries == null ) : "one of queries and limited-by queries must be non-null " ;
244+ assert hasDocumentLevelPermissions () : "document permissions should not contribute to cache key when there is no DLS query " ;
261245 evaluateQueries (context );
262- if (evaluatedQueries != null ) {
263- out .writeBoolean (true );
264- out .writeCollection (evaluatedQueries , StreamOutput ::writeString );
265- } else {
266- out .writeBoolean (false );
267- }
268- if (evaluatedLimitedByQueries != null ) {
269- out .writeBoolean (true );
270- out .writeCollection (evaluatedLimitedByQueries , StreamOutput ::writeString );
271- } else {
272- out .writeBoolean (false );
273- }
246+ out .writeCollection (listOfEvaluatedQueries , StreamOutput ::writeStringCollection );
274247 }
275248
276249 @ Override
277250 public boolean equals (Object o ) {
278251 if (this == o ) return true ;
279252 if (o == null || getClass () != o .getClass ()) return false ;
280253 DocumentPermissions that = (DocumentPermissions ) o ;
281- return Objects .equals (queries , that .queries ) && Objects . equals ( limitedByQueries , that . limitedByQueries );
254+ return Objects .equals (listOfQueries , that .listOfQueries );
282255 }
283256
284257 @ Override
285258 public int hashCode () {
286- return Objects .hash (queries , limitedByQueries );
259+ return Objects .hash (listOfQueries );
287260 }
288261}
0 commit comments