1616
1717/**
1818 * Integration tests for analytics engine index-level authorization via the production SQL plugin
19- * PPL endpoint. Verifies that queries on composite (analytics-engine-backed) indices are subject
20- * to the {@code indices:data/read/analytics/query} permission check.
19+ * PPL endpoint. Verifies that queries on composite (analytics-engine-backed) indices are subject to
20+ * the {@code indices:data/read/analytics/query} permission check.
2121 */
2222public class AnalyticsEngineSecurityIT extends SecurityTestBase {
2323
@@ -55,8 +55,9 @@ private void waitForSecurityPlugin() throws Exception {
5555 try {
5656 Request req = new Request ("GET" , "/_plugins/_security/api/roles" );
5757 RequestOptions .Builder opts = RequestOptions .DEFAULT .toBuilder ();
58- opts .addHeader ("Authorization" , "Basic " +
59- java .util .Base64 .getEncoder ().encodeToString ("admin:admin" .getBytes ()));
58+ opts .addHeader (
59+ "Authorization" ,
60+ "Basic " + java .util .Base64 .getEncoder ().encodeToString ("admin:admin" .getBytes ()));
6061 req .setOptions (opts );
6162 Response resp = client ().performRequest (req );
6263 if (resp .getStatusLine ().getStatusCode () == 200 ) return ;
@@ -74,10 +75,13 @@ private void createTestIndices() throws IOException {
7475 createCompositeIndex (TEST_INDEX );
7576 Request bulk = new Request ("POST" , "/_bulk" );
7677 bulk .addParameter ("refresh" , "true" );
77- bulk .setJsonEntity (String .format (Locale .ROOT ,
78- "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" alice\" , \" age\" : 30}\n "
79- + "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" bob\" , \" age\" : 25}\n " ,
80- TEST_INDEX , TEST_INDEX ));
78+ bulk .setJsonEntity (
79+ String .format (
80+ Locale .ROOT ,
81+ "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" alice\" , \" age\" : 30}\n "
82+ + "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" bob\" , \" age\" : 25}\n " ,
83+ TEST_INDEX ,
84+ TEST_INDEX ));
8185 RequestOptions .Builder opts = RequestOptions .DEFAULT .toBuilder ();
8286 opts .addHeader ("Content-Type" , "application/x-ndjson" );
8387 bulk .setOptions (opts );
@@ -86,17 +90,20 @@ private void createTestIndices() throws IOException {
8690 createCompositeIndex (FORBIDDEN_INDEX );
8791 Request bulkF = new Request ("POST" , "/_bulk" );
8892 bulkF .addParameter ("refresh" , "true" );
89- bulkF .setJsonEntity (String .format (Locale .ROOT ,
90- "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" secret\" , \" age\" : 99}\n " ,
91- FORBIDDEN_INDEX ));
93+ bulkF .setJsonEntity (
94+ String .format (
95+ Locale .ROOT ,
96+ "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" secret\" , \" age\" : 99}\n " ,
97+ FORBIDDEN_INDEX ));
9298 bulkF .setOptions (opts );
9399 client ().performRequest (bulkF );
94100 }
95101
96102 private void createCompositeIndex (String index ) throws IOException {
97103 try {
98104 Request req = new Request ("PUT" , "/" + index );
99- req .setJsonEntity ("""
105+ req .setJsonEntity (
106+ """
100107 {
101108 "settings": {
102109 "number_of_shards": 1,
@@ -121,9 +128,7 @@ private void createSecurityRolesAndUsers() throws IOException {
121128 TEST_INDEX ,
122129 new String [] {"cluster:admin/opensearch/ppl" , "cluster:admin/opensearch/sql" },
123130 new String [] {
124- "indices:data/read*" ,
125- "indices:admin/mappings/get" ,
126- "indices:monitor/settings/get"
131+ "indices:data/read*" , "indices:admin/mappings/get" , "indices:monitor/settings/get"
127132 });
128133 createUser (ALLOWED_USER , ALLOWED_ROLE );
129134
@@ -133,9 +138,7 @@ private void createSecurityRolesAndUsers() throws IOException {
133138 "some_other_index" ,
134139 new String [] {"cluster:admin/opensearch/ppl" , "cluster:admin/opensearch/sql" },
135140 new String [] {
136- "indices:data/read*" ,
137- "indices:admin/mappings/get" ,
138- "indices:monitor/settings/get"
141+ "indices:data/read*" , "indices:admin/mappings/get" , "indices:monitor/settings/get"
139142 });
140143 createUser (DENIED_USER , DENIED_ROLE );
141144
@@ -160,9 +163,7 @@ private void createSecurityRolesAndUsers() throws IOException {
160163 "analytics_security*" ,
161164 new String [] {"cluster:admin/opensearch/ppl" , "cluster:admin/opensearch/sql" },
162165 new String [] {
163- "indices:data/read*" ,
164- "indices:admin/mappings/get" ,
165- "indices:monitor/settings/get"
166+ "indices:data/read*" , "indices:admin/mappings/get" , "indices:monitor/settings/get"
166167 });
167168 createUser (WILDCARD_USER , WILDCARD_ROLE );
168169 }
@@ -172,27 +173,34 @@ public void testPPLQueryAllowedForAuthorizedUser() throws IOException {
172173 // Verify the request passes SecurityFilter (not 403). The query may fail post-auth
173174 // if the backend can't execute, but the FGAC check itself succeeded.
174175 try {
175- JSONObject result = executePPLAsUser (
176- "source = " + TEST_INDEX + " | fields name, age" , ALLOWED_USER );
176+ JSONObject result =
177+ executePPLAsUser ( "source = " + TEST_INDEX + " | fields name, age" , ALLOWED_USER );
177178 assertTrue ("Expected datarows in response" , result .has ("datarows" ));
178179 } catch (ResponseException e ) {
179180 assertNotEquals (
180181 "Expected auth to pass (not 403) for authorized user" ,
181- 403 , e .getResponse ().getStatusLine ().getStatusCode ());
182+ 403 ,
183+ e .getResponse ().getStatusLine ().getStatusCode ());
182184 }
183185 }
184186
185187 @ Test
186188 public void testPPLQueryDeniedForUnauthorizedUser () throws IOException {
187- ResponseException e = assertThrows (ResponseException .class , () ->
188- executePPLAsUser ("source = " + TEST_INDEX + " | fields name, age" , DENIED_USER ));
189+ ResponseException e =
190+ assertThrows (
191+ ResponseException .class ,
192+ () -> executePPLAsUser ("source = " + TEST_INDEX + " | fields name, age" , DENIED_USER ));
189193 assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
190194 }
191195
192196 @ Test
193197 public void testPPLQueryDeniedForForbiddenIndex () throws IOException {
194- ResponseException e = assertThrows (ResponseException .class , () ->
195- executePPLAsUser ("source = " + FORBIDDEN_INDEX + " | fields name, age" , ALLOWED_USER ));
198+ ResponseException e =
199+ assertThrows (
200+ ResponseException .class ,
201+ () ->
202+ executePPLAsUser (
203+ "source = " + FORBIDDEN_INDEX + " | fields name, age" , ALLOWED_USER ));
196204 assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
197205 }
198206
@@ -201,8 +209,12 @@ public void testPPLQueryDeniedWithSearchPermissionOnly() throws IOException {
201209 // User has indices:data/read/search* but NOT indices:data/read/analytics/query.
202210 // The analytics engine dispatches through AnalyticsQueryAction which requires the
203211 // specific analytics/query permission — search permission alone is insufficient.
204- ResponseException e = assertThrows (ResponseException .class , () ->
205- executePPLAsUser ("source = " + TEST_INDEX + " | fields name, age" , SEARCH_ONLY_USER ));
212+ ResponseException e =
213+ assertThrows (
214+ ResponseException .class ,
215+ () ->
216+ executePPLAsUser (
217+ "source = " + TEST_INDEX + " | fields name, age" , SEARCH_ONLY_USER ));
206218 assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
207219 }
208220
@@ -211,36 +223,43 @@ public void testPPLQueryAllowedWithWildcardPermission() throws IOException {
211223 // User's role has index_patterns: ["analytics_security*"] which should match
212224 // "analytics_security_test" via wildcard expansion in the security plugin.
213225 try {
214- JSONObject result = executePPLAsUser (
215- "source = " + TEST_INDEX + " | fields name, age" , WILDCARD_USER );
226+ JSONObject result =
227+ executePPLAsUser ( "source = " + TEST_INDEX + " | fields name, age" , WILDCARD_USER );
216228 assertTrue ("Expected datarows in response" , result .has ("datarows" ));
217229 } catch (ResponseException e ) {
218230 assertNotEquals (
219231 "Expected auth to pass (not 403) for wildcard-permitted user" ,
220- 403 , e .getResponse ().getStatusLine ().getStatusCode ());
232+ 403 ,
233+ e .getResponse ().getStatusLine ().getStatusCode ());
221234 }
222235 }
223236
224237 @ Test
225238 public void testPPLQueryDeniedWithWildcardPermissionOnNonMatchingIndex () throws IOException {
226239 // User's role has index_patterns: ["analytics_security*"] which should NOT match
227240 // "analytics_forbidden_test".
228- ResponseException e = assertThrows (ResponseException .class , () ->
229- executePPLAsUser ("source = " + FORBIDDEN_INDEX + " | fields name, age" , WILDCARD_USER ));
241+ ResponseException e =
242+ assertThrows (
243+ ResponseException .class ,
244+ () ->
245+ executePPLAsUser (
246+ "source = " + FORBIDDEN_INDEX + " | fields name, age" , WILDCARD_USER ));
230247 assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
231248 }
232249
233250 @ Test
234251 public void testSQLQueryAllowedForAuthorizedUser () throws IOException {
235252 try {
236- JSONObject result = executeSQLAsUser (
237- "SELECT name, age FROM " + TEST_INDEX + " LIMIT 3" , ALLOWED_USER );
238- assertTrue ("Expected datarows or schema in response" ,
253+ JSONObject result =
254+ executeSQLAsUser ("SELECT name, age FROM " + TEST_INDEX + " LIMIT 3" , ALLOWED_USER );
255+ assertTrue (
256+ "Expected datarows or schema in response" ,
239257 result .has ("datarows" ) || result .has ("schema" ));
240258 } catch (ResponseException e ) {
241259 assertNotEquals (
242260 "Expected auth to pass (not 403) for authorized user" ,
243- 403 , e .getResponse ().getStatusLine ().getStatusCode ());
261+ 403 ,
262+ e .getResponse ().getStatusLine ().getStatusCode ());
244263 }
245264 }
246265
@@ -252,34 +271,49 @@ public void testSQLQueryAllowedForAuthorizedUser() throws IOException {
252271
253272 @ Test
254273 public void testSQLQueryDeniedForUnauthorizedUser () throws IOException {
255- ResponseException e = assertThrows (ResponseException .class , () ->
256- executeSQLAsUser ("SELECT name, age FROM " + TEST_INDEX + " LIMIT 3" , DENIED_USER ));
257- assertTrue ("Expected 403 or 500 with security exception, got " + e .getResponse ().getStatusLine ().getStatusCode (),
274+ ResponseException e =
275+ assertThrows (
276+ ResponseException .class ,
277+ () ->
278+ executeSQLAsUser ("SELECT name, age FROM " + TEST_INDEX + " LIMIT 3" , DENIED_USER ));
279+ assertTrue (
280+ "Expected 403 or 500 with security exception, got "
281+ + e .getResponse ().getStatusLine ().getStatusCode (),
258282 e .getResponse ().getStatusLine ().getStatusCode () == 403
259- || e .getResponse ().getStatusLine ().getStatusCode () == 500 );
283+ || e .getResponse ().getStatusLine ().getStatusCode () == 500 );
260284 }
261285
262286 @ Test
263287 public void testSQLQueryDeniedForForbiddenIndex () throws IOException {
264- ResponseException e = assertThrows (ResponseException .class , () ->
265- executeSQLAsUser ("SELECT name, age FROM " + FORBIDDEN_INDEX + " LIMIT 3" , ALLOWED_USER ));
266- assertTrue ("Expected 403 or 500 with security exception, got " + e .getResponse ().getStatusLine ().getStatusCode (),
288+ ResponseException e =
289+ assertThrows (
290+ ResponseException .class ,
291+ () ->
292+ executeSQLAsUser (
293+ "SELECT name, age FROM " + FORBIDDEN_INDEX + " LIMIT 3" , ALLOWED_USER ));
294+ assertTrue (
295+ "Expected 403 or 500 with security exception, got "
296+ + e .getResponse ().getStatusLine ().getStatusCode (),
267297 e .getResponse ().getStatusLine ().getStatusCode () == 403
268- || e .getResponse ().getStatusLine ().getStatusCode () == 500 );
298+ || e .getResponse ().getStatusLine ().getStatusCode () == 500 );
269299 }
270300
271301 @ Test
272302 public void testSQLQueryDeniedWithSearchPermissionOnly () throws IOException {
273- ResponseException e = assertThrows (ResponseException .class , () ->
274- executeSQLAsUser ("SELECT name, age FROM " + TEST_INDEX + " LIMIT 3" , SEARCH_ONLY_USER ));
275- assertTrue ("Expected 403 or 500 with security exception, got " + e .getResponse ().getStatusLine ().getStatusCode (),
303+ ResponseException e =
304+ assertThrows (
305+ ResponseException .class ,
306+ () ->
307+ executeSQLAsUser (
308+ "SELECT name, age FROM " + TEST_INDEX + " LIMIT 3" , SEARCH_ONLY_USER ));
309+ assertTrue (
310+ "Expected 403 or 500 with security exception, got "
311+ + e .getResponse ().getStatusLine ().getStatusCode (),
276312 e .getResponse ().getStatusLine ().getStatusCode () == 403
277- || e .getResponse ().getStatusLine ().getStatusCode () == 500 );
313+ || e .getResponse ().getStatusLine ().getStatusCode () == 500 );
278314 }
279315
280- /**
281- * Executes a PPL query via the production SQL plugin endpoint (/_plugins/_ppl).
282- */
316+ /** Executes a PPL query via the production SQL plugin endpoint (/_plugins/_ppl). */
283317 private JSONObject executePPLAsUser (String query , String username ) throws IOException {
284318 Request request = new Request ("POST" , "/_plugins/_ppl" );
285319 request .setJsonEntity (String .format (Locale .ROOT , "{\" query\" : \" %s\" }" , query ));
@@ -295,9 +329,7 @@ private JSONObject executePPLAsUser(String query, String username) throws IOExce
295329 return new JSONObject (body );
296330 }
297331
298- /**
299- * Executes a SQL query via the production SQL plugin endpoint (/_plugins/_sql).
300- */
332+ /** Executes a SQL query via the production SQL plugin endpoint (/_plugins/_sql). */
301333 private JSONObject executeSQLAsUser (String query , String username ) throws IOException {
302334 Request request = new Request ("POST" , "/_plugins/_sql" );
303335 request .setJsonEntity (String .format (Locale .ROOT , "{\" query\" : \" %s\" }" , query ));
0 commit comments