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 43844e1c8a [#11968] docs(auth): document the X-Gravitino-Active-Roles
header (#12365)
43844e1c8a is described below
commit 43844e1c8af2c51650861193d136da511c61f3cf
Author: Bharath Krishna <[email protected]>
AuthorDate: Wed Aug 5 18:52:12 2026 -0700
[#11968] docs(auth): document the X-Gravitino-Active-Roles header (#12365)
### What changes were proposed in this pull request?
Documents the `X-Gravitino-Active-Roles` header, which shipped across
#11966, #11967, and #12095 with no user-facing documentation.
Adds an **Active Roles** section to `docs/security/access-control.md`
covering:
- the grammar — a role name, a comma-separated list, `ALL`, `NONE`, and
an absent or empty value meaning `ALL`;
- the matching rules — names are exact, `ALL` and `NONE` are recognized
only in upper case, whitespace is trimmed and duplicates collapse;
- what narrowing changes — it only ever subtracts, `DENY` stays global,
ownership is untouched, and the narrowed set reaches direct checks, list
filtering, and credential vending alike;
- the two error classes, `400` for a malformed value and `403` for a
role the caller does not hold, and why an unheld role is rejected rather
than ignored;
- how Spark, Trino 481+, and the Java client send the header, and that
the two engine settings are catalog-level and static;
- the scope limit to Gravitino's native authorization path, with a
pointer to Authorization Pushdown.
Also adds a short **Read or Write Scope** note to
`docs/security/credential-vending.md`, since a narrowed caller that no
longer holds `MODIFY_TABLE` is vended a read-only credential.
The wording follows the implementation rather than the design document
in one place: an unrecognized keyword is parsed as an ordinary role name
and therefore returns `403`, not `400`.
### Why are the changes needed?
The feature is complete and enforced on the native and Iceberg REST
paths, but `X-Gravitino-Active-Roles` did not appear anywhere under
`docs/`, so it was undiscoverable to users.
Part of #11968
### Does this PR introduce _any_ user-facing change?
Yes, documentation only. No behavior changes.
### How was this patch tested?
Documentation only, no tests. Verified the Markdown renders correctly
and that the relative links and the `#active-roles` anchor resolve.
Every documented rule was checked against the implementation in
`ActiveRolesParser`, `AuthenticationFilter`,
`GravitinoInterceptionService`, and `IcebergTableOperationExecutor`.
---
docs/security/access-control.md | 79 +++++++++++++++++++++++++++++++++++++
docs/security/credential-vending.md | 4 ++
2 files changed, 83 insertions(+)
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index 538a80abba..c49595a3b2 100755
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -274,6 +274,85 @@ Granting or revoking a privilege on an object takes
`MANAGE_GRANTS` on that obje
Granting or revoking a role, and overriding a role's privileges, takes
`MANAGE_GRANTS` on the
metalake. Setting an owner takes ownership.
+## Narrowing Access with Active Roles
+
+By default, a request is evaluated against every role the caller holds. The
`X-Gravitino-Active-Roles`
+header narrows that set for the request that carries it, so a workload runs
with only the roles it
+needs instead of every role its user has been granted.
+
+```text
+X-Gravitino-Active-Roles: analyst,reader
+```
+
+| Value | Meaning |
+|---------------------|-----------------------------------------------------|
+| `analyst` | Activate one named role |
+| `analyst,reader` | Activate several; access is the union of just these |
+| `ALL` | Activate every role the caller holds |
+| `NONE` | Activate no role |
+| *(absent or empty)* | Same as `ALL` |
+
+Role names are matched exactly, and `ALL` and `NONE` are recognized only in
upper case, so `all` is
+read as the name of a role. Surrounding whitespace is trimmed and repeated
names collapse.
+
+### What Narrowing Changes
+
+Narrowing only ever subtracts. The server validates the declaration against
the roles the caller
+actually holds, so the header can never widen access, and a caller that omits
it is evaluated exactly
+as before.
+
+- **`DENY` stays global.** A deny carried by any role the caller holds still
applies even when that
+ role is not active, so narrowing cannot be used to escape a denial.
+- **Ownership is untouched.** Access that comes from owning an object is
granted to the owner
+ directly rather than through a role, so an owner keeps it even under `NONE`.
+- **Every decision in the request is narrowed**, not only direct checks. List
results are filtered
+ against the active set, and so are the privileges behind credential vending:
an Iceberg caller
+ whose active roles no longer carry `MODIFY_TABLE` is vended a read-only
storage credential in place
+ of a writable one.
+
+### Errors
+
+| Condition |
Response |
+|--------------------------------------------------------------------|-------------------|
+| An empty entry, such as the trailing comma in `analyst,` | `400
Bad Request` |
+| `ALL` or `NONE` combined with anything else, such as `ALL,analyst` | `400
Bad Request` |
+| A well-formed value naming a role the caller does not hold | `403
Forbidden` |
+
+A role that does not exist and a role the caller was never granted both return
`403`, so the response
+cannot be used to discover which role names exist. An unheld role is rejected
rather than ignored,
+which surfaces a typo immediately instead of silently reducing access.
+
+### Sending the Header
+
+Apache Spark forwards any `header.*` catalog property to the Iceberg REST
catalog:
+
+```properties
+spark.sql.catalog.{catalog}.header.X-Gravitino-Active-Roles = analyst
+```
+
+Trino 481 and later forwards headers configured on the catalog:
+
+```properties
+iceberg.rest-catalog.http-headers = X-Gravitino-Active-Roles: analyst
+```
+
+Both are catalog-level and static, so the same value applies to every user and
session using that
+catalog. The Java client sets the header per client instance:
+
+```java
+GravitinoClient.builder(uri)
+ .withMetalake("metalake")
+ .withHeaders(ImmutableMap.of("X-Gravitino-Active-Roles", "analyst"))
+ .build();
+```
+
+### Scope
+
+Narrowing applies where Gravitino enforces authorization itself: the native
REST API and the Iceberg
+REST catalog. Catalogs that push enforcement down to an external system
evaluate against the mapped
+user and groups and never see the declaration, so the header has no effect
there. See
+[Authorization Pushdown](authorization-pushdown.md).
+
## Server Configuration
Settings live in `${GRAVITINO_HOME}/conf/gravitino.conf`. Authorization is off
by default; the
diff --git a/docs/security/credential-vending.md
b/docs/security/credential-vending.md
index 8620b50883..a8277cfa95 100755
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -364,6 +364,10 @@ GET
/api/metalakes/{metalake}/objects/catalog/{catalog}/credentials
The Gravitino Spark and Flink connectors call this for you and inject the
returned credentials, so no client configuration is needed.
+### Read or Write Scope
+
+Over the IRC, a credential is vended for writing when the caller is entitled
to modify the table, and for reading otherwise. Narrowing the caller's roles
with the `X-Gravitino-Active-Roles` header narrows this as well, so a caller
whose active roles no longer carry `MODIFY_TABLE` is vended a read-only
credential. See [Narrowing Access with Active
Roles](access-control.md#narrowing-access-with-active-roles).
+
## Custom Credentials
Gravitino supports custom credentials. You can implement the
`org.apache.gravitino.credential.CredentialProvider` interface to support
custom credentials, and place the corresponding jar in the classpath of the IRC
or the Fileset catalog.