Skip to content

Commit 87f9d76

Browse files
Add FGAC integration tests for aliases, wildcards, and index patterns
Tests verify that the security plugin correctly resolves indices for AnalyticsQueryAction requests (which use the fallback path through IndexNameExpressionResolver since DefaultPlanExecutor does not implement TransportIndicesResolvingAction). New test cases: - Alias-based query access (allowed via alias, denied via concrete name) - Wildcard index pattern in query (allowed when role matches all resolved indices) - Wildcard query denied when user lacks access to resolved indices - Partial access denied (user has alias-only access, wildcard expands beyond) Signed-off-by: Finnegan Carroll <carrofin@amazon.com> Signed-off-by: Finn Carroll <carrofin@amazon.com>
1 parent 4c04a4d commit 87f9d76

1 file changed

Lines changed: 149 additions & 12 deletions

File tree

integ-test/src/test/java/org/opensearch/sql/security/AnalyticsEngineSecurityIT.java

Lines changed: 149 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)