Skip to content

Commit 132f3b1

Browse files
Expand FGAC integration tests: exact permission, aliases, wildcards, index patterns (opensearch-project#5503)
1 parent 797e4fb commit 132f3b1

1 file changed

Lines changed: 158 additions & 12 deletions

File tree

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

Lines changed: 158 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
@@ -216,6 +255,26 @@ public void testPPLQueryDeniedWithSearchPermissionOnly() throws IOException {
216255
executePPLAsUser(
217256
"source = " + TEST_INDEX + " | fields name, age", SEARCH_ONLY_USER));
218257
assertEquals(403, e.getResponse().getStatusLine().getStatusCode());
258+
String body = org.opensearch.sql.legacy.TestUtils.getResponseBody(e.getResponse(), true);
259+
assertTrue(
260+
"Expected response to reference the missing analytics/query action, got: " + body,
261+
body.contains("indices:data/read/analytics/query"));
262+
}
263+
264+
@Test
265+
public void testPPLQueryAllowedWithExactAnalyticsQueryPermission() throws IOException {
266+
// User has exactly indices:data/read/analytics/query (not a broad wildcard).
267+
// Proves this specific permission is sufficient for analytics engine queries.
268+
try {
269+
JSONObject result =
270+
executePPLAsUser("source = " + TEST_INDEX + " | fields name, age", EXACT_PERM_USER);
271+
assertTrue("Expected datarows in response", result.has("datarows"));
272+
} catch (ResponseException e) {
273+
assertNotEquals(
274+
"Expected auth to pass (not 403) for user with exact analytics/query permission",
275+
403,
276+
e.getResponse().getStatusLine().getStatusCode());
277+
}
219278
}
220279

221280
@Test
@@ -247,6 +306,93 @@ public void testPPLQueryDeniedWithWildcardPermissionOnNonMatchingIndex() throws
247306
assertEquals(403, e.getResponse().getStatusLine().getStatusCode());
248307
}
249308

309+
// --- Alias-based access tests ---
310+
311+
@Test
312+
public void testPPLQueryAllowedViaAlias() throws IOException {
313+
// User's role has index_patterns: ["analytics_alias"]. Security plugin resolves the
314+
// alias to the concrete index. Since AnalyticsQueryRequest uses strictExpandOpen(),
315+
// IndexNameExpressionResolver resolves the alias and security matches it against the
316+
// role's index_patterns which includes the alias name.
317+
try {
318+
JSONObject result =
319+
executePPLAsUser("source = " + TEST_ALIAS + " | fields name, age", ALIAS_USER);
320+
assertTrue("Expected datarows in response", result.has("datarows"));
321+
} catch (ResponseException e) {
322+
assertNotEquals(
323+
"Expected auth to pass (not 403) for alias-permitted user",
324+
403,
325+
e.getResponse().getStatusLine().getStatusCode());
326+
}
327+
}
328+
329+
@Test
330+
public void testPPLQueryDeniedViaAliasForUnauthorizedUser() throws IOException {
331+
// DENIED_USER has no access to analytics_alias or the underlying index.
332+
ResponseException e =
333+
assertThrows(
334+
ResponseException.class,
335+
() -> executePPLAsUser("source = " + TEST_ALIAS + " | fields name, age", DENIED_USER));
336+
assertEquals(403, e.getResponse().getStatusLine().getStatusCode());
337+
}
338+
339+
@Test
340+
public void testPPLQueryAllowedViaConcreteIndexForAliasUser() throws IOException {
341+
// ALIAS_USER's role has index_patterns: ["analytics_alias"]. In OpenSearch's security
342+
// model, granting access to an alias also implicitly grants access to the underlying
343+
// concrete index. This verifies the query succeeds via the concrete name.
344+
try {
345+
JSONObject result =
346+
executePPLAsUser("source = " + TEST_INDEX + " | fields name, age", ALIAS_USER);
347+
assertTrue("Expected datarows in response", result.has("datarows"));
348+
} catch (ResponseException e) {
349+
assertNotEquals(
350+
"Expected auth to pass (not 403) for alias user querying concrete index",
351+
403,
352+
e.getResponse().getStatusLine().getStatusCode());
353+
}
354+
}
355+
356+
// --- Wildcard index pattern in query tests ---
357+
358+
@Test
359+
public void testPPLQueryWithWildcardIndexAllowed() throws IOException {
360+
// WILDCARD_USER has index_patterns: ["analytics_security*"]. Query uses wildcard
361+
// "analytics_security*" which resolves to analytics_security_test and
362+
// analytics_security_extra — both match the role's pattern.
363+
try {
364+
JSONObject result =
365+
executePPLAsUser("source = analytics_security* | fields name, age", WILDCARD_USER);
366+
assertTrue("Expected datarows in response", result.has("datarows"));
367+
} catch (ResponseException e) {
368+
assertNotEquals(
369+
"Expected auth to pass (not 403) for wildcard query with matching permissions",
370+
403,
371+
e.getResponse().getStatusLine().getStatusCode());
372+
}
373+
}
374+
375+
@Test
376+
public void testPPLQueryWithWildcardIndexDenied() throws IOException {
377+
// DENIED_USER has no access to any analytics_* indices.
378+
ResponseException e =
379+
assertThrows(
380+
ResponseException.class,
381+
() -> executePPLAsUser("source = analytics_security* | fields name, age", DENIED_USER));
382+
assertEquals(403, e.getResponse().getStatusLine().getStatusCode());
383+
}
384+
385+
@Test
386+
public void testPPLQueryWithWildcardIndexPartialAccessDenied() throws IOException {
387+
// ALIAS_USER only has access to "analytics_alias" — not to "analytics_security*".
388+
// A wildcard query expanding to indices the user lacks permission for should be denied.
389+
ResponseException e =
390+
assertThrows(
391+
ResponseException.class,
392+
() -> executePPLAsUser("source = analytics_security* | fields name, age", ALIAS_USER));
393+
assertEquals(403, e.getResponse().getStatusLine().getStatusCode());
394+
}
395+
250396
@Test
251397
public void testSQLQueryAllowedForAuthorizedUser() throws IOException {
252398
try {

0 commit comments

Comments
 (0)