1414import org .jetbrains .annotations .NotNull ;
1515import org .json .JSONObject ;
1616import org .junit .jupiter .api .BeforeAll ;
17- import org .junit .jupiter .api .Test ;
17+ import org .junit .jupiter .api .BeforeEach ;
1818import org .junit .jupiter .api .TestInstance ;
1919import org .junit .jupiter .api .TestInstance .Lifecycle ;
20+ import org .junit .jupiter .params .ParameterizedTest ;
21+ import org .junit .jupiter .params .provider .ValueSource ;
2022import org .opensearch .client .Request ;
2123import org .opensearch .client .RequestOptions ;
2224import org .opensearch .client .Response ;
@@ -51,7 +53,7 @@ public class FGACIndexScanningIT extends SecurityTestBase {
5153 private static final String SECURE_LOGS = "secure_logs_fgac" ;
5254 private static final String EMPLOYEE_RECORDS = "employee_records_fgac" ;
5355
54- private static final int LARGE_DATASET_SIZE = 2000 ;
56+ private static final int LARGE_DATASET_SIZE = 100 ;
5557
5658 @ SneakyThrows
5759 @ BeforeAll
@@ -64,18 +66,30 @@ public void initialize() {
6466 @ Override
6567 protected void init () throws Exception {
6668 super .init ();
67- enableCalcite ();
6869 allowCalciteFallback ();
6970 }
7071
72+ /**
73+ * Configures the query engine for the test.
74+ *
75+ * @param useCalcite true to use V3 (Calcite) engine, false to use V2 (legacy) engine
76+ */
77+ private void configureEngine (boolean useCalcite ) throws IOException {
78+ if (useCalcite ) {
79+ enableCalcite ();
80+ } else {
81+ disableCalcite ();
82+ }
83+ }
84+
7185 private void setupTestIndices () throws IOException {
7286 createPublicLogsIndex ();
7387 createSensitiveLogsIndex ();
7488 createEmployeeRecordsIndex ();
7589 createSecureLogsIndex ();
7690 }
7791
78- /** Creates public_logs index with 2000+ documents. */
92+ /** Creates public_logs index with test documents. */
7993 private void createPublicLogsIndex () throws IOException {
8094 Request request = new Request ("PUT" , "/" + PUBLIC_LOGS );
8195 request .setJsonEntity (
@@ -99,7 +113,7 @@ private void createPublicLogsIndex() throws IOException {
99113 bulkInsertDocs (PUBLIC_LOGS , "public" );
100114 }
101115
102- /** Creates sensitive_logs index with 2000+ documents. */
116+ /** Creates sensitive_logs index with test documents. */
103117 private void createSensitiveLogsIndex () throws IOException {
104118 Request request = new Request ("PUT" , "/" + SENSITIVE_LOGS );
105119 request .setJsonEntity (
@@ -177,8 +191,6 @@ private void createSecureLogsIndex() throws IOException {
177191 """ );
178192 client ().performRequest (request );
179193
180- // Insert documents with mixed security levels
181- // 1000 public, 500 internal, 500 confidential
182194 bulkInsertDocsWithSecurityLevel ();
183195 }
184196
@@ -255,8 +267,12 @@ private static String getBulkEmployeeIndexRequest() {
255267 private void bulkInsertDocsWithSecurityLevel () throws IOException {
256268 StringBuilder bulk = new StringBuilder ();
257269
258- // 1000 public documents
259- for (int i = 0 ; i < 1000 ; i ++) {
270+ int publicCount = LARGE_DATASET_SIZE / 2 ;
271+ int internalStart = publicCount ;
272+ int internalCount = LARGE_DATASET_SIZE / 4 ;
273+ int confidentialStart = internalStart + internalCount ;
274+
275+ for (int i = 0 ; i < publicCount ; i ++) {
260276 bulk .append (
261277 String .format (
262278 Locale .ROOT ,
@@ -268,8 +284,7 @@ private void bulkInsertDocsWithSecurityLevel() throws IOException {
268284 i ));
269285 }
270286
271- // 500 internal documents
272- for (int i = 1000 ; i < 1500 ; i ++) {
287+ for (int i = internalStart ; i < confidentialStart ; i ++) {
273288 bulk .append (
274289 String .format (
275290 Locale .ROOT ,
@@ -281,8 +296,7 @@ private void bulkInsertDocsWithSecurityLevel() throws IOException {
281296 i ));
282297 }
283298
284- // 500 confidential documents
285- for (int i = 1500 ; i < 2000 ; i ++) {
299+ for (int i = confidentialStart ; i < LARGE_DATASET_SIZE ; i ++) {
286300 bulk .append (
287301 String .format (
288302 Locale .ROOT ,
@@ -345,17 +359,21 @@ private void createRoleWithFieldLevelSecurity() throws IOException {
345359 createRoleWithFLS (MANAGER_ROLE , EMPLOYEE_RECORDS , RECORDS_INDEX_COLUMNS );
346360 }
347361
348- @ Test
349- public void testPublicUserCanAccessPublicLogs () throws IOException {
350- // public_user can access public_logs (large dataset triggers background scanning)
362+ @ ParameterizedTest
363+ @ ValueSource (booleans = {false , true })
364+ public void testPublicUserCanAccessPublicLogs (boolean useCalcite ) throws IOException {
365+ configureEngine (useCalcite );
366+ // public_user can access public_logs
351367 JSONObject result =
352368 executeQueryAsUser (
353369 String .format ("search source=%s | fields message | head 10" , PUBLIC_LOGS ), PUBLIC_USER );
354370 verifyColumn (result , columnName ("message" ));
355371 }
356372
357- @ Test
358- public void testPublicUserCannotAccessSensitiveLogs () throws IOException {
373+ @ ParameterizedTest
374+ @ ValueSource (booleans = {false , true })
375+ public void testPublicUserCannotAccessSensitiveLogs (boolean useCalcite ) throws IOException {
376+ configureEngine (useCalcite );
359377 // public_user cannot access sensitive_logs (should fail at planning stage)
360378 try {
361379 executeQueryAsUser (
@@ -370,8 +388,10 @@ public void testPublicUserCannotAccessSensitiveLogs() throws IOException {
370388 }
371389 }
372390
373- @ Test
374- public void testSensitiveUserCanAccessSensitiveLogs () throws IOException {
391+ @ ParameterizedTest
392+ @ ValueSource (booleans = {false , true })
393+ public void testSensitiveUserCanAccessSensitiveLogs (boolean useCalcite ) throws IOException {
394+ configureEngine (useCalcite );
375395 // sensitive_user can access sensitive_logs
376396 JSONObject result =
377397 executeQueryAsUser (
@@ -380,8 +400,10 @@ public void testSensitiveUserCanAccessSensitiveLogs() throws IOException {
380400 verifyColumn (result , columnName ("message" ));
381401 }
382402
383- @ Test
384- public void testSensitiveUserCannotAccessPublicLogs () throws IOException {
403+ @ ParameterizedTest
404+ @ ValueSource (booleans = {false , true })
405+ public void testSensitiveUserCannotAccessPublicLogs (boolean useCalcite ) throws IOException {
406+ configureEngine (useCalcite );
385407 // sensitive_user cannot access public_logs
386408 try {
387409 executeQueryAsUser (
@@ -396,8 +418,11 @@ public void testSensitiveUserCannotAccessPublicLogs() throws IOException {
396418 }
397419 }
398420
399- @ Test
400- public void testHrUserCanSeeAllFieldsIncludingSensitiveData () throws IOException {
421+ @ ParameterizedTest
422+ @ ValueSource (booleans = {false , true })
423+ public void testHrUserCanSeeAllFieldsIncludingSensitiveData (boolean useCalcite )
424+ throws IOException {
425+ configureEngine (useCalcite );
401426 // hr_user can see ALL fields including sensitive ssn
402427 String queryAllFields =
403428 String .format (
@@ -421,8 +446,10 @@ public void testHrUserCanSeeAllFieldsIncludingSensitiveData() throws IOException
421446 assertTrue ("hr_user should see 'department' field" , hrHasDepartment );
422447 }
423448
424- @ Test
425- public void testManagerUserCannotSeeSensitiveFields () throws IOException {
449+ @ ParameterizedTest
450+ @ ValueSource (booleans = {false , true })
451+ public void testManagerUserCannotSeeSensitiveFields (boolean useCalcite ) throws IOException {
452+ configureEngine (useCalcite );
426453 // manager_user can see most fields but NOT ssn
427454 String queryAllowedFields =
428455 String .format (
@@ -452,8 +479,10 @@ public void testManagerUserCannotSeeSensitiveFields() throws IOException {
452479 managerHasSSN );
453480 }
454481
455- @ Test
456- public void testManagerUserCannotQueryRestrictedField () throws IOException {
482+ @ ParameterizedTest
483+ @ ValueSource (booleans = {false , true })
484+ public void testManagerUserCannotQueryRestrictedField (boolean useCalcite ) throws IOException {
485+ configureEngine (useCalcite );
457486 // Verify manager_user cannot even reference ssn in query (field is invisible)
458487 try {
459488 String queryWithSSN =
@@ -471,8 +500,11 @@ public void testManagerUserCannotQueryRestrictedField() throws IOException {
471500 }
472501 }
473502
474- @ Test
475- public void testFieldLevelSecurityEnforcedWithLargeDataset () throws IOException {
503+ @ ParameterizedTest
504+ @ ValueSource (booleans = {false , true })
505+ public void testFieldLevelSecurityEnforcedWithLargeDataset (boolean useCalcite )
506+ throws IOException {
507+ configureEngine (useCalcite );
476508 // Verify with large result set that FLS is still enforced
477509 String queryLargeDataset =
478510 String .format (
@@ -490,15 +522,16 @@ public void testFieldLevelSecurityEnforcedWithLargeDataset() throws IOException
490522 }
491523
492524 assertFalse (
493- "SECURITY VIOLATION: manager_user should NOT see 'ssn' even with large dataset (2000+ "
494- + " rows). Field-level security must be enforced." ,
525+ "SECURITY VIOLATION: manager_user should NOT see 'ssn' even with large dataset. "
526+ + "Field-level security must be enforced." ,
495527 hasSSNInLarge );
496528 }
497529
498- @ Test
499- public void testRowLevelSecurityV2 () throws IOException {
500- // Test V2 (legacy) engine explicitly
501- disableCalcite ();
530+ @ ParameterizedTest
531+ @ ValueSource (booleans = {false , true })
532+ public void testRowLevelSecurity (boolean useCalcite ) throws IOException {
533+ configureEngine (useCalcite );
534+ String engineLabel = useCalcite ? "V3" : "V2" ;
502535
503536 // limited_user should only see "public" documents
504537
@@ -534,93 +567,36 @@ public void testRowLevelSecurityV2() throws IOException {
534567 }
535568
536569 assertFalse (
537- "[V2] SECURITY VIOLATION: limited_user should NOT see 'confidential' documents. "
538- + "This indicates ThreadContext is not being copied to async worker threads in V2, "
539- + "causing queries to run with admin permissions and bypass row-level security." ,
540- sawConfidential );
541-
542- assertFalse (
543- "[V2] SECURITY VIOLATION: limited_user should NOT see 'internal' documents. "
544- + "This indicates ThreadContext is not being copied to async worker threads in V2, "
545- + "causing queries to run with admin permissions and bypass row-level security." ,
546- sawInternal );
547-
548- assertEquals (
549- "[V2] limited_user should ONLY see 'public' documents (~1000). "
550- + "Seeing more indicates row-level security is being bypassed in V2." ,
551- 1000 ,
552- publicDocs );
553-
554- assertEquals (
555- "[V2] Total visible documents should be ~1000 (only public). "
556- + "Seeing 2000 documents indicates row-level security is completely bypassed in V2." ,
557- 1000 ,
558- totalDocs );
559- }
560-
561- @ Test
562- public void testRowLevelSecurity () throws IOException {
563- // Test V3 (Calcite) engine - Calcite is enabled in init()
564- // limited_user should only see "public" documents
565-
566- // Execute query as limited_user
567- String query =
568570 String .format (
569- "search source=%s | fields security_level, message | stats count() by security_level" ,
570- SECURE_LOGS );
571- JSONObject result = executeQueryAsUser (query , LIMITED_USER );
572-
573- // Extract the datarows for validation
574- var datarows = result .getJSONArray ("datarows" );
575-
576- // limited_user should ONLY see "public" documents
577- // Note: Without DLS configured in Security Plugin, all documents are visible
578- // Once DLS is configured with a rule like: { "match": { "security_level": "public" } }
579- // Then with the ThreadContext fix, this test should pass
580-
581- // Count total documents visible
582- int totalDocs = 0 ;
583- boolean sawConfidential = false ;
584- boolean sawInternal = false ;
585- int publicDocs = 0 ;
586-
587- for (int i = 0 ; i < datarows .length (); i ++) {
588- var row = datarows .getJSONArray (i );
589- int count = row .getInt (0 );
590- String securityLevel = row .getString (1 );
591- totalDocs += count ;
592-
593- if ("confidential" .equals (securityLevel )) {
594- sawConfidential = true ;
595- } else if ("internal" .equals (securityLevel )) {
596- sawInternal = true ;
597- } else if ("public" .equals (securityLevel )) {
598- publicDocs = count ;
599- }
600- }
601-
602- assertFalse (
603- "[V3] SECURITY VIOLATION: limited_user should NOT see 'confidential' documents. "
604- + "This indicates ThreadContext is not being properly copied to search threads in V3, "
605- + "causing queries to run with admin permissions and bypass row-level security." ,
571+ "[%s] SECURITY VIOLATION: limited_user should NOT see 'confidential' documents. "
572+ + "This indicates ThreadContext is not being copied to async worker threads, "
573+ + "causing queries to run with admin permissions and bypass row-level security." ,
574+ engineLabel ),
606575 sawConfidential );
607576
608577 assertFalse (
609- "[V3] SECURITY VIOLATION: limited_user should NOT see 'internal' documents. "
610- + "This indicates ThreadContext is not being properly copied to search threads in V3, "
611- + "causing queries to run with admin permissions and bypass row-level security." ,
578+ String .format (
579+ "[%s] SECURITY VIOLATION: limited_user should NOT see 'internal' documents. "
580+ + "This indicates ThreadContext is not being copied to async worker threads, "
581+ + "causing queries to run with admin permissions and bypass row-level security." ,
582+ engineLabel ),
612583 sawInternal );
613584
585+ int expectedPublicDocs = LARGE_DATASET_SIZE / 2 ;
614586 assertEquals (
615- "[V3] limited_user should ONLY see 'public' documents (~1000). "
616- + "Seeing more indicates row-level security is being bypassed in V3." ,
617- 1000 ,
587+ String .format (
588+ "[%s] limited_user should ONLY see 'public' documents (half of dataset). "
589+ + "Seeing more indicates row-level security is being bypassed." ,
590+ engineLabel ),
591+ expectedPublicDocs ,
618592 publicDocs );
619593
620594 assertEquals (
621- "[V3] Total visible documents should be ~1000 (only public). "
622- + "Seeing 2000 documents indicates row-level security is completely bypassed in V3." ,
623- 1000 ,
595+ String .format (
596+ "[%s] Total visible documents should match public documents only. "
597+ + "Seeing all documents indicates row-level security is completely bypassed." ,
598+ engineLabel ),
599+ expectedPublicDocs ,
624600 totalDocs );
625601 }
626602}
0 commit comments