This is an automated email from the ASF dual-hosted git repository.
roryqi 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 dde85468c9 [#11968] test(iceberg): cover role narrowing in Iceberg
credential vending (#12461)
dde85468c9 is described below
commit dde85468c9f00150e213aaf43df03176baa799ab
Author: Bharath Krishna <[email protected]>
AuthorDate: Fri Aug 14 02:04:23 2026 -0700
[#11968] test(iceberg): cover role narrowing in Iceberg credential vending
(#12461)
### What changes were proposed in this pull request?
Covers role narrowing in the Iceberg REST credential vending path, which
had no integration test.
`IcebergAuthorizationIT` can now register a second Spark catalog that
sends `X-Gravitino-Active-Roles`, so the same caller can be exercised
with and without narrowing. The per-catalog configuration moved into a
helper; the new catalog is opt-in through
`narrowedCatalogActiveRoles()`, mirroring the existing
`supportsCredentialVending()` hook, so the other subclasses are
unaffected.
The test asserts the WRITE to READ downgrade: the write succeeds through
the default catalog and is refused by the storage layer through the
narrowed one, while the read still succeeds. It lives on
`IcebergRESTCloudTokenAuthorizationBaseIT`, so every cloud subclass
inherits it; MinIO runs it in CI.
The narrowed role carries `USE_CATALOG` and `USE_SCHEMA` itself, because
narrowing deactivates the `USE_SCHEMA` role that `@BeforeEach` grants
under a random name.
### Why are the changes needed?
`RoleAssumptionAuthorizationIT` covers the header grammar and
`listCatalogs` filtering, but nothing covered credential vending, so the
privilege-to-credential mapping under narrowing had no protection
against regressions.
Fix: #11968
### Does this PR introduce _any_ user-facing change?
No. Test only.
### How was this patch tested?
MinIO, in both modes:
```
./gradlew :iceberg:iceberg-rest-server:test --tests
"*IcebergRESTMinIOTokenAuthorizationIT*" \
-PskipTests -PtestMode=deploy -PskipDockerTests=false
```
| Test | Result |
| --- | --- |
| MinIO deploy | 4 pass |
| MinIO embedded | 4 pass |
| IcebergTableAuthorizationIT deploy | 16 pass |
The sibling run checks that the shared base class change did not regress
the other Iceberg authorization ITs.
The test is self-controlling: the same user, table and Spark session
write successfully through the default catalog and fail through the
narrowed one, and the narrowed catalog still reads, so the only
difference is the header.
---
.../integration/test/IcebergAuthorizationIT.java | 50 +++++++++++++++++-----
.../IcebergRESTCloudTokenAuthorizationBaseIT.java | 50 ++++++++++++++++++++++
2 files changed, 90 insertions(+), 10 deletions(-)
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
index e16da44ff0..bd69a06659 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergAuthorizationIT.java
@@ -37,6 +37,7 @@ import org.apache.gravitino.Configs;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.auth.AuthConstants;
import org.apache.gravitino.authorization.Owner;
import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
import org.apache.gravitino.client.GravitinoMetalake;
@@ -76,6 +77,7 @@ public class IcebergAuthorizationIT extends BaseIT {
protected static final String METALAKE_NAME = "test_metalake";
protected static final String GRAVITINO_CATALOG_NAME = "iceberg";
protected static final String SPARK_CATALOG_NAME = "rest";
+ protected static final String NARROWED_SPARK_CATALOG_NAME = "narrowed";
protected static final String SUPER_USER = "super";
protected static final String NORMAL_USER = "normal";
@@ -209,6 +211,14 @@ public class IcebergAuthorizationIT extends BaseIT {
return false;
}
+ /**
+ * Roles sent as {@code X-Gravitino-Active-Roles} by the {@link
#NARROWED_SPARK_CATALOG_NAME}
+ * catalog. Returning null registers no such catalog.
+ */
+ protected String narrowedCatalogActiveRoles() {
+ return null;
+ }
+
void revokeUserRoles() {
List<String> roles =
metalakeClientWithAllPrivilege.getUser(NORMAL_USER).roles();
if (roles.size() > 0) {
@@ -292,24 +302,44 @@ public class IcebergAuthorizationIT extends BaseIT {
.set(
"spark.sql.extensions",
"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions")
- .set("spark.sql.catalog.rest",
"org.apache.iceberg.spark.SparkCatalog")
- .set("spark.sql.catalog.rest.type", "rest")
- .set("spark.sql.catalog.rest.uri", icebergRESTUri)
- // disable spark side table cache to check the privilege in each
operation
- .set("spark.sql.catalog.rest." + CatalogProperties.CACHE_ENABLED,
"false")
- .set("spark.sql.catalog.rest.rest.auth.type", "basic")
- .set("spark.sql.catalog.rest.rest.auth.basic.username",
NORMAL_USER)
- .set("spark.sql.catalog.rest.rest.auth.basic.password", "mock")
// drop Iceberg table purge may hang in spark local mode
.set("spark.locality.wait.node", "0");
- if (supportsCredentialVending()) {
+ configureRestCatalog(sparkConf, SPARK_CATALOG_NAME, icebergRESTUri);
+
+ String activeRoles = narrowedCatalogActiveRoles();
+ if (activeRoles != null) {
+ configureRestCatalog(sparkConf, NARROWED_SPARK_CATALOG_NAME,
icebergRESTUri);
sparkConf.set(
- "spark.sql.catalog.rest.header.X-Iceberg-Access-Delegation",
"vended-credentials");
+ sparkCatalogPrefix(NARROWED_SPARK_CATALOG_NAME)
+ + ".header."
+ + AuthConstants.X_GRAVITINO_ACTIVE_ROLES_HEADER,
+ activeRoles);
}
sparkSession =
SparkSession.builder().master("local[1]").config(sparkConf).getOrCreate();
}
+ private static String sparkCatalogPrefix(String catalogName) {
+ return "spark.sql.catalog." + catalogName;
+ }
+
+ private void configureRestCatalog(
+ SparkConf sparkConf, String catalogName, String icebergRESTUri) {
+ String prefix = sparkCatalogPrefix(catalogName);
+ sparkConf
+ .set(prefix, "org.apache.iceberg.spark.SparkCatalog")
+ .set(prefix + ".type", "rest")
+ .set(prefix + ".uri", icebergRESTUri)
+ // disable spark side table cache to check the privilege in each
operation
+ .set(prefix + "." + CatalogProperties.CACHE_ENABLED, "false")
+ .set(prefix + ".rest.auth.type", "basic")
+ .set(prefix + ".rest.auth.basic.username", NORMAL_USER)
+ .set(prefix + ".rest.auth.basic.password", "mock");
+ if (supportsCredentialVending()) {
+ sparkConf.set(prefix + ".header.X-Iceberg-Access-Delegation",
"vended-credentials");
+ }
+ }
+
private String getPGUri() {
return
containerSuite.getPostgreSQLContainer().getJdbcUrl(TestDatabaseName.PG_ICEBERG_AUTHZ_IT);
}
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTCloudTokenAuthorizationBaseIT.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTCloudTokenAuthorizationBaseIT.java
index 583be10d55..c7912761b1 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTCloudTokenAuthorizationBaseIT.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTCloudTokenAuthorizationBaseIT.java
@@ -54,6 +54,12 @@ import org.junit.jupiter.api.Test;
public abstract class IcebergRESTCloudTokenAuthorizationBaseIT extends
IcebergAuthorizationIT {
protected static final String SCHEMA_NAME = "schema";
+ private static final String NARROWED_ROLE_NAME = "narrowed_select_role";
+
+ @Override
+ protected String narrowedCatalogActiveRoles() {
+ return NARROWED_ROLE_NAME;
+ }
@BeforeEach
void revokePrivilege() {
@@ -214,6 +220,50 @@ public abstract class
IcebergRESTCloudTokenAuthorizationBaseIT extends IcebergAu
Assertions.assertEquals(2, rows.size());
}
+ @Test
+ void testActiveRolesNarrowCloudToken() {
+ String tableName = "test_narrowed_" + getCloudProviderName();
+ createTable(SCHEMA_NAME, tableName);
+
+ grantNarrowedSelectRole();
+ grantModifyTableRole(tableName);
+
+ // Every held role is active, so the vended credential can write.
+ sql("INSERT INTO %s VALUES (1,1),(2,2)", tableName);
+
+ // Narrowed to the select-only role the caller gets a read-only
credential, so the write fails
+ // inside Spark instead of being rejected by Gravitino.
+ Assertions.assertThrows(
+ SparkException.class,
+ () ->
+ sql(
+ "INSERT INTO %s.%s.%s VALUES (3,3)",
+ NARROWED_SPARK_CATALOG_NAME, SCHEMA_NAME, tableName));
+
+ List<Object[]> rows =
+ sql("SELECT * FROM %s.%s.%s", NARROWED_SPARK_CATALOG_NAME,
SCHEMA_NAME, tableName);
+ Assertions.assertEquals(2, rows.size());
+ }
+
+ /**
+ * The narrowed catalog names this role in a static header, so it has to
carry every privilege the
+ * read path needs; narrowing deactivates the per-test USE_SCHEMA role.
+ */
+ private void grantNarrowedSelectRole() {
+ SecurableObject catalogObject =
+ SecurableObjects.ofCatalog(
+ GRAVITINO_CATALOG_NAME,
ImmutableList.of(Privileges.UseCatalog.allow()));
+ SecurableObject schemaObject =
+ SecurableObjects.ofSchema(
+ catalogObject,
+ SCHEMA_NAME,
+ ImmutableList.of(Privileges.UseSchema.allow(),
Privileges.SelectTable.allow()));
+ metalakeClientWithAllPrivilege.createRole(
+ NARROWED_ROLE_NAME, new HashMap<>(), ImmutableList.of(catalogObject,
schemaObject));
+ metalakeClientWithAllPrivilege.grantRolesToUser(
+ ImmutableList.of(NARROWED_ROLE_NAME), NORMAL_USER);
+ }
+
protected void grantUseSchemaRole(String schema) {
String roleName = "useSchema_" + UUID.randomUUID();
List<SecurableObject> securableObjects = new ArrayList<>();