@@ -23,6 +23,8 @@ public class AnalyticsEngineSecurityIT extends SecurityTestBase {
2323
2424 private static final String TEST_INDEX = "analytics_security_test" ;
2525 private static final String FORBIDDEN_INDEX = "analytics_forbidden_test" ;
26+ private static final String TEST_INDEX_2 = "analytics_security_extra" ;
27+ private static final String TEST_ALIAS = "analytics_alias" ;
2628
2729 private static final String ALLOWED_USER = "analytics_allowed_user" ;
2830 private static final String ALLOWED_ROLE = "analytics_allowed_role" ;
@@ -32,6 +34,10 @@ public class AnalyticsEngineSecurityIT extends SecurityTestBase {
3234 private static final String SEARCH_ONLY_ROLE = "analytics_search_only_role" ;
3335 private static final String WILDCARD_USER = "analytics_wildcard_user" ;
3436 private static final String WILDCARD_ROLE = "analytics_wildcard_role" ;
37+ private static final String ALIAS_USER = "analytics_alias_user" ;
38+ private static final String ALIAS_ROLE = "analytics_alias_role" ;
39+ private static final String EXACT_PERM_USER = "analytics_exact_perm_user" ;
40+ private static final String EXACT_PERM_ROLE = "analytics_exact_perm_role" ;
3541
3642 private static boolean initialized = false ;
3743
@@ -73,30 +79,39 @@ private void createTestIndices() throws IOException {
7379 // Create composite (analytics-engine-backed) indices so the SQL plugin routes
7480 // queries through the analytics engine's DefaultPlanExecutor.
7581 createCompositeIndex (TEST_INDEX );
82+ createCompositeIndex (TEST_INDEX_2 );
83+ createCompositeIndex (FORBIDDEN_INDEX );
84+
85+ RequestOptions .Builder opts = RequestOptions .DEFAULT .toBuilder ();
86+ opts .addHeader ("Content-Type" , "application/x-ndjson" );
87+
7688 Request bulk = new Request ("POST" , "/_bulk" );
7789 bulk .addParameter ("refresh" , "true" );
7890 bulk .setJsonEntity (
7991 String .format (
8092 Locale .ROOT ,
8193 "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" alice\" , \" age\" : 30}\n "
82- + "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" bob\" , \" age\" : 25}\n " ,
94+ + "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" bob\" , \" age\" : 25}\n "
95+ + "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" carol\" , \" age\" : 28}\n "
96+ + "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" secret\" , \" age\" : 99}\n " ,
8397 TEST_INDEX ,
84- TEST_INDEX ));
85- RequestOptions . Builder opts = RequestOptions . DEFAULT . toBuilder ();
86- opts . addHeader ( "Content-Type" , "application/x-ndjson" );
98+ TEST_INDEX ,
99+ TEST_INDEX_2 ,
100+ FORBIDDEN_INDEX ) );
87101 bulk .setOptions (opts );
88102 client ().performRequest (bulk );
89103
90- createCompositeIndex (FORBIDDEN_INDEX );
91- Request bulkF = new Request ("POST" , "/_bulk" );
92- bulkF .addParameter ("refresh" , "true" );
93- bulkF .setJsonEntity (
104+ // Create alias pointing to TEST_INDEX
105+ Request aliasReq = new Request ("POST" , "/_aliases" );
106+ aliasReq .setJsonEntity (
94107 String .format (
95108 Locale .ROOT ,
96- "{\" index\" : {\" _index\" : \" %s\" }}\n {\" name\" : \" secret\" , \" age\" : 99}\n " ,
97- FORBIDDEN_INDEX ));
98- bulkF .setOptions (opts );
99- client ().performRequest (bulkF );
109+ """
110+ {"actions": [{"add": {"index": "%s", "alias": "%s"}}]}
111+ """ ,
112+ TEST_INDEX ,
113+ TEST_ALIAS ));
114+ client ().performRequest (aliasReq );
100115 }
101116
102117 private void createCompositeIndex (String index ) throws IOException {
@@ -166,6 +181,30 @@ private void createSecurityRolesAndUsers() throws IOException {
166181 "indices:data/read*" , "indices:admin/mappings/get" , "indices:monitor/settings/get"
167182 });
168183 createUser (WILDCARD_USER , WILDCARD_ROLE );
184+
185+ // Role with access only to the alias — verifies security plugin resolves alias to
186+ // concrete index and permits access when role's index_patterns matches the alias name.
187+ createRoleWithPermissions (
188+ ALIAS_ROLE ,
189+ TEST_ALIAS ,
190+ new String [] {"cluster:admin/opensearch/ppl" , "cluster:admin/opensearch/sql" },
191+ new String [] {
192+ "indices:data/read*" , "indices:admin/mappings/get" , "indices:monitor/settings/get"
193+ });
194+ createUser (ALIAS_USER , ALIAS_ROLE );
195+
196+ // Role with exactly indices:data/read/analytics/query — proves this specific permission
197+ // is both necessary and sufficient for analytics engine queries.
198+ createRoleWithPermissions (
199+ EXACT_PERM_ROLE ,
200+ TEST_INDEX ,
201+ new String [] {"cluster:admin/opensearch/ppl" , "cluster:admin/opensearch/sql" },
202+ new String [] {
203+ "indices:data/read/analytics/query" ,
204+ "indices:admin/mappings/get" ,
205+ "indices:monitor/settings/get"
206+ });
207+ createUser (EXACT_PERM_USER , EXACT_PERM_ROLE );
169208 }
170209
171210 @ Test
@@ -218,6 +257,22 @@ public void testPPLQueryDeniedWithSearchPermissionOnly() throws IOException {
218257 assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
219258 }
220259
260+ @ Test
261+ public void testPPLQueryAllowedWithExactAnalyticsQueryPermission () throws IOException {
262+ // User has exactly indices:data/read/analytics/query (not a broad wildcard).
263+ // Proves this specific permission is sufficient for analytics engine queries.
264+ try {
265+ JSONObject result =
266+ executePPLAsUser ("source = " + TEST_INDEX + " | fields name, age" , EXACT_PERM_USER );
267+ assertTrue ("Expected datarows in response" , result .has ("datarows" ));
268+ } catch (ResponseException e ) {
269+ assertNotEquals (
270+ "Expected auth to pass (not 403) for user with exact analytics/query permission" ,
271+ 403 ,
272+ e .getResponse ().getStatusLine ().getStatusCode ());
273+ }
274+ }
275+
221276 @ Test
222277 public void testPPLQueryAllowedWithWildcardPermission () throws IOException {
223278 // User's role has index_patterns: ["analytics_security*"] which should match
@@ -247,6 +302,88 @@ public void testPPLQueryDeniedWithWildcardPermissionOnNonMatchingIndex() throws
247302 assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
248303 }
249304
305+ // --- Alias-based access tests ---
306+
307+ @ Test
308+ public void testPPLQueryAllowedViaAlias () throws IOException {
309+ // User's role has index_patterns: ["analytics_alias"]. Security plugin resolves the
310+ // alias to the concrete index. Since AnalyticsQueryRequest uses strictExpandOpen(),
311+ // IndexNameExpressionResolver resolves the alias and security matches it against the
312+ // role's index_patterns which includes the alias name.
313+ try {
314+ JSONObject result =
315+ executePPLAsUser ("source = " + TEST_ALIAS + " | fields name, age" , ALIAS_USER );
316+ assertTrue ("Expected datarows in response" , result .has ("datarows" ));
317+ } catch (ResponseException e ) {
318+ assertNotEquals (
319+ "Expected auth to pass (not 403) for alias-permitted user" ,
320+ 403 ,
321+ e .getResponse ().getStatusLine ().getStatusCode ());
322+ }
323+ }
324+
325+ @ Test
326+ public void testPPLQueryDeniedViaAliasForUnauthorizedUser () throws IOException {
327+ // DENIED_USER has no access to analytics_alias or the underlying index.
328+ ResponseException e =
329+ assertThrows (
330+ ResponseException .class ,
331+ () -> executePPLAsUser ("source = " + TEST_ALIAS + " | fields name, age" , DENIED_USER ));
332+ assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
333+ }
334+
335+ @ Test
336+ public void testPPLQueryDeniedViaConcreteIndexForAliasOnlyUser () throws IOException {
337+ // ALIAS_USER's role only has index_patterns: ["analytics_alias"].
338+ // Querying the concrete index name directly should be denied because the role
339+ // does not match the concrete index name "analytics_security_test".
340+ ResponseException e =
341+ assertThrows (
342+ ResponseException .class ,
343+ () -> executePPLAsUser ("source = " + TEST_INDEX + " | fields name, age" , ALIAS_USER ));
344+ assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
345+ }
346+
347+ // --- Wildcard index pattern in query tests ---
348+
349+ @ Test
350+ public void testPPLQueryWithWildcardIndexAllowed () throws IOException {
351+ // WILDCARD_USER has index_patterns: ["analytics_security*"]. Query uses wildcard
352+ // "analytics_security*" which resolves to analytics_security_test and
353+ // analytics_security_extra — both match the role's pattern.
354+ try {
355+ JSONObject result =
356+ executePPLAsUser ("source = analytics_security* | fields name, age" , WILDCARD_USER );
357+ assertTrue ("Expected datarows in response" , result .has ("datarows" ));
358+ } catch (ResponseException e ) {
359+ assertNotEquals (
360+ "Expected auth to pass (not 403) for wildcard query with matching permissions" ,
361+ 403 ,
362+ e .getResponse ().getStatusLine ().getStatusCode ());
363+ }
364+ }
365+
366+ @ Test
367+ public void testPPLQueryWithWildcardIndexDenied () throws IOException {
368+ // DENIED_USER has no access to any analytics_* indices.
369+ ResponseException e =
370+ assertThrows (
371+ ResponseException .class ,
372+ () -> executePPLAsUser ("source = analytics_security* | fields name, age" , DENIED_USER ));
373+ assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
374+ }
375+
376+ @ Test
377+ public void testPPLQueryWithWildcardIndexPartialAccessDenied () throws IOException {
378+ // ALIAS_USER only has access to "analytics_alias" — not to "analytics_security*".
379+ // A wildcard query expanding to indices the user lacks permission for should be denied.
380+ ResponseException e =
381+ assertThrows (
382+ ResponseException .class ,
383+ () -> executePPLAsUser ("source = analytics_security* | fields name, age" , ALIAS_USER ));
384+ assertEquals (403 , e .getResponse ().getStatusLine ().getStatusCode ());
385+ }
386+
250387 @ Test
251388 public void testSQLQueryAllowedForAuthorizedUser () throws IOException {
252389 try {
0 commit comments