This is an automated email from the ASF dual-hosted git repository.
bharos pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 155ba1e532 [#11968] test(auth): add integration test for
X-Gravitino-Active-Roles (#12175)
155ba1e532 is described below
commit 155ba1e532a0e11a15dbc2db8dc8e0138cbbf54a
Author: Bharath Krishna <[email protected]>
AuthorDate: Tue Aug 4 14:36:56 2026 -0700
[#11968] test(auth): add integration test for X-Gravitino-Active-Roles
(#12175)
### What changes were proposed in this pull request?
Adds `RoleAssumptionAuthorizationIT`, an end-to-end integration test for
the `X-Gravitino-Active-Roles` header.
The test user holds two roles, each granting only `USE_CATALOG` on a
different catalog, so the result of `listCatalogs` is a direct
observation of which roles the server activated for the request. The
catalogs are `MODEL` catalogs, so no external metastore or container is
required.
| Header value | Expectation |
| --- | --- |
| absent | all held roles active, both catalogs visible |
| `ALL` | same as absent |
| `role_catalog1` | only `catalog1` visible |
| `role_catalog2` | only `catalog2` visible |
| `role_catalog1,role_catalog2` | both catalogs visible |
| `NONE` | no catalog visible |
| `role_not_held` | `403`, message names the unheld role |
| `ALL,role_catalog1` | `400`, message names the header |
The `400` case is asserted over raw HTTP (`java.net.http.HttpClient`) so
that the status code itself is verified rather than the client-side
exception mapping.
### Why are the changes needed?
`X-Gravitino-Active-Roles` is covered by unit tests, but nothing
exercised the full path from client header through the authentication
filter, role validation, and privilege filtering. This closes that gap.
Part of #11968. This PR covers the list-filtering half of the subtask;
the Iceberg credential-vending ITs and the user documentation follow in
separate PRs, so the issue stays open.
### Does this PR introduce _any_ user-facing change?
No. Test-only.
### How was this patch tested?
The new integration test, verified green in CI on all three `BackendIT`
legs (h2/embedded, mysql/deploy, postgresql/deploy). Tests are
order-independent, and every client the test creates is closed via
try-with-resources.
---
.../RoleAssumptionAuthorizationIT.java | 171 +++++++++++++++++++++
1 file changed, 171 insertions(+)
diff --git
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/RoleAssumptionAuthorizationIT.java
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/RoleAssumptionAuthorizationIT.java
new file mode 100644
index 0000000000..31b4a03075
--- /dev/null
+++
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/RoleAssumptionAuthorizationIT.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.client.integration.test.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.auth.AuthConstants;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.SecurableObjects;
+import org.apache.gravitino.client.GravitinoAdminClient;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration test for role assumption via the {@code
X-Gravitino-Active-Roles} header. The test
+ * user holds two roles, each granting {@code USE_CATALOG} on a different
catalog, and the header
+ * activates a subset of them per request. Covers the default {@code ALL}, a
narrowed {@code NAMED}
+ * subset (one role and several), {@code NONE}, a {@code 403} for an unheld
role, and a {@code 400}
+ * for a malformed header.
+ */
+@Tag("gravitino-docker-test")
+public class RoleAssumptionAuthorizationIT extends BaseRestApiAuthorizationIT {
+
+ private static final String CATALOG1 = "catalog1";
+ private static final String CATALOG2 = "catalog2";
+ private static final String ROLE_CATALOG1 = "role_catalog1";
+ private static final String ROLE_CATALOG2 = "role_catalog2";
+ private static final String UNHELD_ROLE = "role_not_held";
+
+ @BeforeAll
+ public void startIntegrationTest() throws Exception {
+ super.startIntegrationTest();
+ GravitinoMetalake admin = client.loadMetalake(METALAKE);
+ // Model catalogs need no external system; only their visibility in
listCatalogs matters here.
+ admin.createCatalog(CATALOG1, Catalog.Type.MODEL, "model", "comment", new
HashMap<>());
+ admin.createCatalog(CATALOG2, Catalog.Type.MODEL, "model", "comment", new
HashMap<>());
+
+ // One role per catalog, each granting only USE_CATALOG on that catalog.
+ SecurableObject useCatalog1 =
+ SecurableObjects.ofCatalog(CATALOG1,
ImmutableList.of(Privileges.UseCatalog.allow()));
+ SecurableObject useCatalog2 =
+ SecurableObjects.ofCatalog(CATALOG2,
ImmutableList.of(Privileges.UseCatalog.allow()));
+ admin.createRole(ROLE_CATALOG1, new HashMap<>(),
ImmutableList.of(useCatalog1));
+ admin.createRole(ROLE_CATALOG2, new HashMap<>(),
ImmutableList.of(useCatalog2));
+ admin.grantRolesToUser(ImmutableList.of(ROLE_CATALOG1, ROLE_CATALOG2),
NORMAL_USER);
+ }
+
+ /** Builds a client for {@link #NORMAL_USER} that sends the given
active-roles header value. */
+ private GravitinoAdminClient userClientWithActiveRoles(String headerValue) {
+ return GravitinoAdminClient.builder(serverUri)
+ .withSimpleAuth(NORMAL_USER)
+
.withHeaders(ImmutableMap.of(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER,
headerValue))
+ .build();
+ }
+
+ private String[] sortedCatalogs(GravitinoAdminClient userClient) {
+ String[] catalogs = userClient.loadMetalake(METALAKE).listCatalogs();
+ Arrays.sort(catalogs);
+ return catalogs;
+ }
+
+ @Test
+ public void testAllRolesActiveByDefault() {
+ // No header (or ALL) activates every held role, so both catalogs are
visible.
+ assertArrayEquals(new String[] {CATALOG1, CATALOG2},
sortedCatalogs(normalUserClient));
+ try (GravitinoAdminClient all = userClientWithActiveRoles("ALL")) {
+ assertArrayEquals(new String[] {CATALOG1, CATALOG2},
sortedCatalogs(all));
+ }
+ }
+
+ @Test
+ public void testNarrowToSingleRole() {
+ // Activating a single role narrows the visible catalogs to that role's
catalog.
+ try (GravitinoAdminClient onlyCatalog1 =
userClientWithActiveRoles(ROLE_CATALOG1)) {
+ assertArrayEquals(new String[] {CATALOG1}, sortedCatalogs(onlyCatalog1));
+ }
+ try (GravitinoAdminClient onlyCatalog2 =
userClientWithActiveRoles(ROLE_CATALOG2)) {
+ assertArrayEquals(new String[] {CATALOG2}, sortedCatalogs(onlyCatalog2));
+ }
+ }
+
+ @Test
+ public void testNarrowToMultipleRoles() {
+ // A comma-separated list activates every named role.
+ try (GravitinoAdminClient both =
+ userClientWithActiveRoles(ROLE_CATALOG1 + "," + ROLE_CATALOG2)) {
+ assertArrayEquals(new String[] {CATALOG1, CATALOG2},
sortedCatalogs(both));
+ }
+ }
+
+ @Test
+ public void testNoneActivatesNothing() {
+ // NONE activates no role, so no catalog is visible.
+ try (GravitinoAdminClient none = userClientWithActiveRoles("NONE")) {
+ assertArrayEquals(new String[0], sortedCatalogs(none));
+ }
+ }
+
+ @Test
+ public void testUnheldRoleReturns403() {
+ // Declaring a role the caller does not hold is rejected before
authorization.
+ try (GravitinoAdminClient unheld = userClientWithActiveRoles(UNHELD_ROLE))
{
+ ForbiddenException exception =
+ assertThrows(
+ ForbiddenException.class, () ->
unheld.loadMetalake(METALAKE).listCatalogs());
+ assertTrue(
+ exception.getMessage().contains("cannot assume active role"),
+ "Unexpected message: " + exception.getMessage());
+ assertTrue(
+ exception.getMessage().contains(UNHELD_ROLE),
+ "Unexpected message: " + exception.getMessage());
+ }
+ }
+
+ @Test
+ public void testMalformedHeaderReturns400() throws Exception {
+ // A reserved keyword combined with a role name is syntactically invalid.
Sent over raw HTTP so
+ // that the status code itself is asserted rather than the client's
exception mapping.
+ String authHeader =
+ AuthConstants.AUTHORIZATION_BASIC_HEADER
+ + Base64.getEncoder()
+ .encodeToString((NORMAL_USER +
":dummy").getBytes(StandardCharsets.UTF_8));
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .uri(new URI(serverUri + "/api/metalakes/" + METALAKE +
"/catalogs"))
+ .header(AuthConstants.HTTP_HEADER_AUTHORIZATION, authHeader)
+ .header(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER, "ALL," +
ROLE_CATALOG1)
+ .GET()
+ .build();
+ HttpResponse<String> response =
+ HttpClient.newHttpClient().send(request,
HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(400, response.statusCode(), "Unexpected body: " +
response.body());
+ assertTrue(
+
response.body().contains(AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER),
+ "Unexpected body: " + response.body());
+ }
+}