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
The example creates a role that can read one schema and gives it to a group. Reading one schema takes
three privileges: USE_CATALOG and USE_SCHEMA to reach it, and SELECT_TABLE to read what is in it.
- UI
- REST
-
Create the role. Open Authorize → Roles, click + Role, enter the Role name, and click Add role. Members can also be chosen here, under Member type.
-
Grant privileges on the catalog. Open the role, select its Privileges tab, and click + Privilege. Choose Catalog as the Object type and the catalog as the Object, leave the Effect at Allow, select Use Catalog, and click Grant privilege. The privilege list fills in once an Object is chosen, and offers only what can be granted on that object.
-
Grant privileges on the schema. Click + Privilege again, choose Schema and the schema, select Use Schema and Select Table, and click Grant privilege.
-
Give the role to a group. Select the role's Members tab, click + Members, choose Groups, select the group, and click Add members. Granting to a group reaches every member, which is how access is usually managed for a team.
Adding a user or group to a role also adds it to the metalake, so assigning a role is how someone from the server's directory gets access to a metalake. To give several users the same roles at once, select them on Authorize → Users and click + Roles.
-
Create the role with its privileges. Every object the role names must exist, or the role is not created.
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"}
]
}
]
}' -
Give the role to a group. 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"]}'
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()));