1111import java .io .IOException ;
1212import java .util .Locale ;
1313import lombok .SneakyThrows ;
14+ import org .apache .hc .core5 .http .ParseException ;
15+ import org .apache .hc .core5 .http .io .entity .EntityUtils ;
1416import org .jetbrains .annotations .NotNull ;
1517import org .json .JSONObject ;
1618import org .junit .jupiter .api .BeforeAll ;
@@ -81,15 +83,15 @@ private void configureEngine(boolean useCalcite) throws IOException {
8183 }
8284 }
8385
84- private void setupTestIndices () throws IOException {
86+ private void setupTestIndices () throws IOException , ParseException {
8587 createPublicLogsIndex ();
8688 createSensitiveLogsIndex ();
8789 createEmployeeRecordsIndex ();
8890 createSecureLogsIndex ();
8991 }
9092
9193 /** Creates public_logs index with test documents. */
92- private void createPublicLogsIndex () throws IOException {
94+ private void createPublicLogsIndex () throws IOException , ParseException {
9395 Request request = new Request ("PUT" , "/" + PUBLIC_LOGS );
9496 request .setJsonEntity (
9597 """
@@ -113,7 +115,7 @@ private void createPublicLogsIndex() throws IOException {
113115 }
114116
115117 /** Creates sensitive_logs index with test documents. */
116- private void createSensitiveLogsIndex () throws IOException {
118+ private void createSensitiveLogsIndex () throws IOException , ParseException {
117119 Request request = new Request ("PUT" , "/" + SENSITIVE_LOGS );
118120 request .setJsonEntity (
119121 """
@@ -140,7 +142,7 @@ private void createSensitiveLogsIndex() throws IOException {
140142 * Creates employee_records index with sensitive fields for field-level security testing. Contains
141143 * fields: employee_id, name, department, salary, ssn
142144 */
143- private void createEmployeeRecordsIndex () throws IOException {
145+ private void createEmployeeRecordsIndex () throws IOException , ParseException {
144146 Request request = new Request ("PUT" , "/" + EMPLOYEE_RECORDS );
145147 request .setJsonEntity (
146148 """
@@ -170,7 +172,7 @@ private void createEmployeeRecordsIndex() throws IOException {
170172 * Creates secure_logs index with mixed security levels. This index contains documents with
171173 * different security_level values to test row-level filtering.
172174 */
173- private void createSecureLogsIndex () throws IOException {
175+ private void createSecureLogsIndex () throws IOException , ParseException {
174176 Request request = new Request ("PUT" , "/" + SECURE_LOGS );
175177 request .setJsonEntity (
176178 """
@@ -194,7 +196,7 @@ private void createSecureLogsIndex() throws IOException {
194196 }
195197
196198 /** Bulk inserts documents to trigger background scanning. */
197- private void bulkInsertDocs (String indexName , String prefix ) throws IOException {
199+ private void bulkInsertDocs (String indexName , String prefix ) throws IOException , ParseException {
198200 StringBuilder bulk = new StringBuilder ();
199201 for (int i = 0 ; i < FGACIndexScanningIT .LARGE_DATASET_SIZE ; i ++) {
200202 bulk .append (
@@ -219,10 +221,13 @@ private void bulkInsertDocs(String indexName, String prefix) throws IOException
219221
220222 Response response = client ().performRequest (request );
221223 assertEquals (200 , response .getStatusLine ().getStatusCode ());
224+ String body = EntityUtils .toString (response .getEntity ());
225+ JSONObject json = new JSONObject (body );
226+ assertFalse ("Bulk indexing reported errors: " + body , json .getBoolean ("errors" ));
222227 }
223228
224229 /** Bulk inserts employee records with sensitive fields for FLS testing. */
225- private void bulkInsertEmployeeRecords () throws IOException {
230+ private void bulkInsertEmployeeRecords () throws IOException , ParseException {
226231 String bulk = getBulkEmployeeIndexRequest ();
227232
228233 Request request = new Request ("POST" , "/_bulk" );
@@ -235,6 +240,9 @@ private void bulkInsertEmployeeRecords() throws IOException {
235240
236241 Response response = client ().performRequest (request );
237242 assertEquals (200 , response .getStatusLine ().getStatusCode ());
243+ String body = EntityUtils .toString (response .getEntity ());
244+ JSONObject json = new JSONObject (body );
245+ assertFalse ("Bulk indexing reported errors: " + body , json .getBoolean ("errors" ));
238246 }
239247
240248 @ NotNull
@@ -263,7 +271,7 @@ private static String getBulkEmployeeIndexRequest() {
263271 }
264272
265273 /** Bulk inserts documents with different security levels for row-level testing. */
266- private void bulkInsertDocsWithSecurityLevel () throws IOException {
274+ private void bulkInsertDocsWithSecurityLevel () throws IOException , ParseException {
267275 StringBuilder bulk = new StringBuilder ();
268276
269277 int publicCount = LARGE_DATASET_SIZE / 2 ;
@@ -307,6 +315,26 @@ private void bulkInsertDocsWithSecurityLevel() throws IOException {
307315 i ));
308316 }
309317
318+ // Add document with null security_level to test DLS behavior with null values
319+ bulk .append (
320+ String .format (
321+ Locale .ROOT ,
322+ """
323+ { "index": { "_index": "%s" } }
324+ { "message": "null security level", "security_level": null, "timestamp": "2025-01-01T00:00:00Z" }
325+ """ ,
326+ FGACIndexScanningIT .SECURE_LOGS ));
327+
328+ // Add document with missing security_level field to test DLS behavior with missing fields
329+ bulk .append (
330+ String .format (
331+ Locale .ROOT ,
332+ """
333+ { "index": { "_index": "%s" } }
334+ { "message": "missing security level", "timestamp": "2025-01-01T00:00:00Z" }
335+ """ ,
336+ FGACIndexScanningIT .SECURE_LOGS ));
337+
310338 Request request = new Request ("POST" , "/_bulk" );
311339 request .addParameter ("refresh" , "true" );
312340 request .setJsonEntity (bulk .toString ());
@@ -317,6 +345,9 @@ private void bulkInsertDocsWithSecurityLevel() throws IOException {
317345
318346 Response response = client ().performRequest (request );
319347 assertEquals (200 , response .getStatusLine ().getStatusCode ());
348+ String body = EntityUtils .toString (response .getEntity ());
349+ JSONObject json = new JSONObject (body );
350+ assertFalse ("Bulk indexing reported errors: " + body , json .getBoolean ("errors" ));
320351 }
321352
322353 /** Creates security roles and users for testing. */
@@ -505,25 +536,44 @@ public void testFieldLevelSecurityEnforcedWithLargeDataset(boolean useCalcite)
505536 throws IOException {
506537 configureEngine (useCalcite );
507538 // Verify with large result set that FLS is still enforced
539+ // Query all data (not just count) to actually exercise FLS at scale
508540 String queryLargeDataset =
509541 String .format (
510- "search source=%s | fields name, salary, department | stats count() " , EMPLOYEE_RECORDS );
542+ "search source=%s | fields name, salary, department, employee_id " , EMPLOYEE_RECORDS );
511543 JSONObject managerLargeResult = executeQueryAsUser (queryLargeDataset , MANAGER_USER );
512544
513- // Even with large dataset, manager should not see ssn
545+ // Verify the schema contains allowed fields but not restricted fields
514546 var largeSchema = managerLargeResult .getJSONArray ("schema" );
515- boolean hasSSNInLarge = false ;
547+ boolean hasName = false ;
548+ boolean hasSalary = false ;
549+ boolean hasDepartment = false ;
550+ boolean hasEmployeeId = false ;
551+ boolean hasSSN = false ;
552+
516553 for (int i = 0 ; i < largeSchema .length (); i ++) {
517- if ("ssn" .equals (largeSchema .getJSONObject (i ).getString ("name" ))) {
518- hasSSNInLarge = true ;
519- break ;
520- }
554+ String fieldName = largeSchema .getJSONObject (i ).getString ("name" );
555+ if ("name" .equals (fieldName )) hasName = true ;
556+ if ("salary" .equals (fieldName )) hasSalary = true ;
557+ if ("department" .equals (fieldName )) hasDepartment = true ;
558+ if ("employee_id" .equals (fieldName )) hasEmployeeId = true ;
559+ if ("ssn" .equals (fieldName )) hasSSN = true ;
521560 }
522561
562+ // Verify allowed fields are present
563+ assertTrue ("manager_user should see 'name' field in large dataset query" , hasName );
564+ assertTrue ("manager_user should see 'salary' field in large dataset query" , hasSalary );
565+ assertTrue ("manager_user should see 'department' field in large dataset query" , hasDepartment );
566+ assertTrue ("manager_user should see 'employee_id' field in large dataset query" , hasEmployeeId );
567+
568+ // Verify restricted field is NOT present
523569 assertFalse (
524570 "SECURITY VIOLATION: manager_user should NOT see 'ssn' even with large dataset. "
525571 + "Field-level security must be enforced." ,
526- hasSSNInLarge );
572+ hasSSN );
573+
574+ // Verify we actually got data back (not just an empty result)
575+ var datarows = managerLargeResult .getJSONArray ("datarows" );
576+ assertTrue ("Expected to receive data from large dataset query" , datarows .length () > 0 );
527577 }
528578
529579 @ ParameterizedTest
@@ -544,6 +594,21 @@ public void testRowLevelSecurity(boolean useCalcite) throws IOException {
544594 // Extract the datarows for validation
545595 var datarows = result .getJSONArray ("datarows" );
546596
597+ // Derive column indexes from schema to support both V2 and V3 engines
598+ var schema = result .getJSONArray ("schema" );
599+ int countIdx = -1 ;
600+ int levelIdx = -1 ;
601+ for (int i = 0 ; i < schema .length (); i ++) {
602+ String name = schema .getJSONObject (i ).getString ("name" );
603+ if ("security_level" .equals (name )) {
604+ levelIdx = i ;
605+ } else if ("count()" .equalsIgnoreCase (name ) || "count" .equalsIgnoreCase (name )) {
606+ countIdx = i ;
607+ }
608+ }
609+ assertTrue ("Expected count() in schema" , countIdx >= 0 );
610+ assertTrue ("Expected security_level in schema" , levelIdx >= 0 );
611+
547612 // Count total documents visible
548613 int totalDocs = 0 ;
549614 boolean sawConfidential = false ;
@@ -552,8 +617,8 @@ public void testRowLevelSecurity(boolean useCalcite) throws IOException {
552617
553618 for (int i = 0 ; i < datarows .length (); i ++) {
554619 var row = datarows .getJSONArray (i );
555- int count = row .getInt (0 );
556- String securityLevel = row .getString (1 );
620+ int count = row .getInt (countIdx );
621+ String securityLevel = row .getString (levelIdx );
557622 totalDocs += count ;
558623
559624 if ("confidential" .equals (securityLevel )) {
0 commit comments