Roles
Overview
A role is a named set of privileges, granted to users and groups. Privileges are never granted directly to a user, so a role is the only way a privilege reaches anyone.
A role holds objects, and for each one a list of privileges, each carrying an ALLOW or DENY
condition. A privilege binds only to object types it supports, so CREATE_TABLE binds to a metalake,
catalog, or schema, never to a table. Whoever creates a role owns it, and can alter or delete it.
How a request is evaluated against the roles a caller holds, including how DENY interacts with
ALLOW, is described in Privileges.
Quick start
-
Create a role. Reading one schema takes three privileges:
USE_CATALOGandUSE_SCHEMAto reach it, andSELECT_TABLEto read what is in it. Object names are not validated, so a typo produces a role that grants nothing.Create a schema reader rolecurl -sS -X POST "https://{gravitino_host}/api/metalakes/{metalake}/roles" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "schema_reader",
"properties": {},
"securableObjects": [
{
"fullName": "{catalog}",
"type": "CATALOG",
"privileges": [{"name": "USE_CATALOG", "condition": "ALLOW"}]
},
{
"fullName": "{catalog}.{schema}",
"type": "SCHEMA",
"privileges": [
{"name": "USE_SCHEMA", "condition": "ALLOW"},
{"name": "SELECT_TABLE", "condition": "ALLOW"}
]
}
]
}' -
Grant it to a group. Granting to a group reaches every member, which is how access is usually managed for a team. Granting to a single user works the same way with
usersin the path.Grant the role to a groupcurl -sS -X PUT "https://{gravitino_host}/api/metalakes/{metalake}/permissions/groups/{group}/grant" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"roleNames": ["schema_reader"]}'
Managing roles in the UI
Creating a role
- Open Authorize → Roles and click + Role.
- Enter a Role name.
- Optionally choose members now: pick Users or Groups under Member type and select them.
- Click Add role.
Granting privileges to a role
- Open the role from Authorize → Roles and select its Privileges tab.
- Click + Privilege.
- Choose the Object type, such as Catalog, Schema, or Table, then the Object.
- Choose the Effect: Allow or Deny.
- Select the Privileges. The list fills in once an Object is chosen, and offers the privileges that can be granted on that object.
- Click Grant privilege.
Assigning the role
- Open the role and select its Members tab.
- Click + Members, choose Users or Groups, and select them.
- Click Add members.
The list offers every user and group in the server's directory. Adding a user or group to a role also adds it to the metalake, so assigning a role is how someone from the directory gets access to a metalake.
To give several users the same roles at once, select them on Authorize → Users and click + Roles.
Privileges
See Privileges for a comprehensive list of privilege grants, and Required privileges for what each operation needs.
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.
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.
DENYstays 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_TABLEis 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:
spark.sql.catalog.{catalog}.header.X-Gravitino-Active-Roles = analyst
Trino 481 and later forwards headers configured on the catalog:
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:
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.
Endpoints
Paths are relative to https://{gravitino_host}/api/metalakes/{metalake}. For request and response schemas, see the Gravitino REST API.
| Operation | Method | Path |
|---|---|---|
| Create a role | POST | /roles |
| List roles | GET | /roles |
| Get a role | GET | /roles/{role} |
| Delete a role | DELETE | /roles/{role} |
| Grant privileges to a role | PUT | /permissions/roles/{role}/{object_type}/{object_name}/grant |
| Revoke privileges from a role | PUT | /permissions/roles/{role}/{object_type}/{object_name}/revoke |
| Replace a role's privileges | PUT | /permissions/roles/{role}/ |
| List the roles bound to an object | GET | /objects/{object_type}/{object_name}/roles |
Add ?details=true to the list path to get full roles instead of names. Granting a role to a user or
group is covered on Users and Groups.
Replacing a role's privileges is destructive: afterwards the role holds exactly what the request body contains, and any object absent from it is dropped.
Java client
Most calls map directly onto a GravitinoClient method and are covered by the
Java doc.
Two take arguments that are hard to derive from the signature alone.
A role's objects are built as a nested path rather than a dotted string:
SecurableObject table =
SecurableObjects.ofTable(
SecurableObjects.ofSchema(
SecurableObjects.ofCatalog("catalog1", Collections.emptyList()),
"schema1",
Collections.emptyList()),
"table1",
Lists.newArrayList(Privileges.SelectTable.allow()));
Role role = client.createRole("schema_reader", ImmutableMap.of(), Lists.newArrayList(table));
Privileges are passed as a Set, and each carries its condition. The List overloads of these two
methods are deprecated:
MetadataObject schema =
MetadataObjects.of(Lists.newArrayList("catalog1", "schema1"), MetadataObject.Type.SCHEMA);
client.grantPrivilegesToRole("schema_reader", schema, ImmutableSet.of(Privileges.SelectTable.allow()));
client.revokePrivilegesFromRole("schema_reader", schema, ImmutableSet.of(Privileges.SelectTable.deny()));