roryqi commented on code in PR #11790: URL: https://github.com/apache/gravitino/pull/11790#discussion_r3489851153
########## design-docs/gravitino-role-assumption.md: ########## @@ -0,0 +1,334 @@ +<!-- + 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. +--> + +# Design: Role Assumption (SET ROLE) — Narrowing Effective Permissions + +| Field | Value | +| ---------- | ------------------------------------------------------------------------- | +| Status | Draft | +| Authors | @bharos | +| Created | 2026-06-24 | +| Discussion | [#10894](https://github.com/apache/gravitino/discussions/10894) | +| Scope | Native authorization path (Iceberg REST catalog + native Gravitino API) | + +--- + +## 1. Summary + +When a user holds multiple roles, Gravitino always enforces the union of all of them. A workload has no +way to run with a narrower subset of the access its identity could reach. + +This document proposes **role assumption** — the analog of Snowflake's `USE ROLE` and Hive's `SET ROLE`. +A caller declares which role(s) should be active for a request, Gravitino verifies the caller actually +holds them, and authorization is evaluated against only that active set. The feature can only *reduce* a +caller's effective permissions, never expand them. If no role is declared, behavior is exactly as it is +today. + +The mechanism is intentionally simple: a request header, `X-Gravitino-Active-Role`, carries the active +role(s), and the server narrows enforcement to match — across access checks, list results, and credential +vending alike. Section 3 defines the header and its values; Section 4 explains how the server enforces +it. Transport is the easy part — the common instinct is to treat this as "just a Trino change," but the +substance is server-side. + +--- + +## 2. Motivation + +### 2.1 The problem + +A pipeline or AI agent that only needs 5 tables still runs with access to every table (say 105) its +identity can reach. If that workload misbehaves — a logic bug, a bad query, a leaked credential, or an +LLM agent over-reaching — the blast radius is everything the identity could touch, and out-of-scope +access **succeeds silently** instead of being denied and surfaced. + +### 2.2 Why it matters now + +A few things make this worth doing now. It gives workloads real runtime least-privilege: a job can drop +to exactly the access it needs without anyone having to mint a separate narrowly-scoped identity for it. +It also produces a much cleaner audit signal — with narrowing on, out-of-scope access turns into a hard +deny you can alert on, instead of a successful-but-unexpected access buried in the logs. AI agents are +the sharpest version of the same need: you want an agent to operate inside a declared, minimal scope and +to fail the moment it steps outside it. There's also a concrete migration angle — Hive's SQL Standard +Authorization supports `SET ROLE`, so teams moving HMS tables to Iceberg behind Gravitino lose that +capability today, and this restores it. + +### 2.3 Goals + +- Let a caller narrow the active role set for the native authorization path (Iceberg REST + native + API). +- Guarantee narrowing is **subtractive only** — it can never grant access the caller lacks. +- Apply the narrowing consistently everywhere authorization is consulted: direct access checks **and** + list-result filtering **and** credential vending. +- Be **fully backward compatible**: no declared role ⇒ today's union behavior, byte-for-byte. + +### 2.4 Non-goals (initial) + +- Dynamic in-session `SET ROLE` switching mid-connection (deferred to a later phase — see Section 11). +- Narrowing for **pushdown-authorized** catalogs (Hive/JDBC via Ranger/JDBC plugins), where + enforcement happens in the external system — see Section 6. +- Changing how ownership is modeled (its *interaction* with narrowing is an explicit open decision — + see Section 5). +- Write/`CREATE` semantics (which role owns newly created objects) — flagged for forward-compat only. + +--- + +## 3. The active-role header + +The interface is a single request header: + +``` +X-Gravitino-Active-Role: <value> +``` + +The caller sets it to declare which of its roles should be active for that request. The server validates +the value against the roles the caller actually holds, then evaluates authorization against only those +roles. + +### 3.1 Accepted values + +A value is either a role name, a comma-separated list of role names, or one of the reserved keywords +`ALL` / `NONE`: + +| Value | Meaning | +|---|---| +| `<role>` (e.g. `analyst`) | Activate a single named role. | +| `<role>,<role>` (e.g. `analyst,reader`) | Activate a list of roles; effective access is the union of just these. | +| `ALL` | Activate every role the caller holds — identical to today's behavior. | +| `NONE` | Activate no roles; all role-derived access is denied. | +| *(header absent)* | Same as `ALL` — fully backward compatible. | + +Here `analyst` and `reader` are example role names, not keywords; only `ALL` and `NONE` are reserved. +This mirrors the vocabulary users already know from Hive (`SET ROLE role | ALL | NONE`) and Snowflake +(`USE SECONDARY ROLES ALL | NONE`). + +### 3.2 Examples + +A reporting job that should only ever read through its `analyst` role: + +``` +GET /iceberg/v1/namespaces/sales/tables +X-Gravitino-Active-Role: analyst +``` + +Declaring a role the caller does not actually hold is rejected — this is what keeps the feature +subtractive: + +``` +X-Gravitino-Active-Role: admin → 403 Forbidden (caller is not a member of admin) +``` + +### 3.3 Semantics + +- **Subtractive only.** The server validates membership first, so the header can never grant access the + caller lacks — at most it removes roles from the evaluated set. +- **Consistent everywhere.** The narrowed set applies to every authorization decision in the request: + direct access checks, the filtering of list results, and the privileges used for credential vending. +- **Backward compatible.** No header (or `ALL`) means today's union behavior, unchanged. + +--- + +## 4. How the server enforces narrowing + +Without an `X-Gravitino-Active-Role` header, Gravitino evaluates each operation against the user's full +set of roles — those granted directly plus those inherited from groups. The header narrows that set for +the request: + +1. **Resolve** the caller's effective roles the normal way — direct grants plus group-inherited ones. + Say that's `{analyst, editor, auditor}`. +2. **Validate** the header against that set. The declared role must be one the caller actually holds; a + role they don't hold is rejected. This is the guardrail that keeps narrowing *subtractive* — you can + reduce what you use, never assume a role you were never granted. +3. **Enforce** using only the validated active role(s). With `X-Gravitino-Active-Role: analyst`, the + request is evaluated as `analyst` alone; `editor` and `auditor` are simply not consulted. + +So the active set is `(roles named in the header) ∩ (the caller's effective roles)` — always a subset, +never a way to widen or switch into a role you don't hold. Because it keys on the role and not how the +role was acquired, a role held only through a group works exactly the same — group-based authorization is +covered with nothing extra. + +This needs **no change to the Casbin model**. Authorization is already checked with the user as the +subject, and a grouping rule expands the user to all their roles — that expansion is the union. Since +policies are written against *roles*, the same enforcer is instead called with each active *role* as the +subject. Narrowing is just that change of subject, and deny rules run over the same active subset, so +deny-wins is preserved — an explicit deny on an active role still denies. + +The one identity-derived exception is **ownership**: access a user (or their group) gets from *owning* an +object comes from the ownership grant, not a role, so under the proposed default it still applies when +roles are narrowed. Whether narrowing should also suppress owner access is the open decision in Section 5. + +Two implementation properties keep this correct. Narrowing is applied by *choosing which roles to +evaluate* for the request — never by editing the user's bindings, since the enforcers are shared +process-wide across all users. And it must reach **every** decision: list filtering and credential +vending go through the same check as direct access, so a narrowed caller lists only what its active roles +allow and is never vended a broader storage credential — provided vending is wired in deliberately, or +the narrowing could be bypassed through the storage token. + +--- + +## 5. Ownership — the key open decision + +There is one place where narrowing is not automatic, and it needs a decision from the community. In +Gravitino, object **ownership** grants access directly to the owning user or group, independent of +roles. Because most operations are allowed if the caller either holds a granting role *or* owns the +object, an owner stays authorized no matter which roles are active. + +So narrowing roles does not, on its own, narrow the access a caller derives from ownership. This is the +one spot where Gravitino differs from Snowflake, where ownership flows through the active (primary) +role. The community needs to choose the semantics: + +- **Option A — ownership always applies.** Narrowing affects role-granted privileges only; you keep + access to objects you own. Simplest and least surprising for existing deployments, but a narrowed + workload could still reach objects it owns outside its declared scope. +- **Option B — ownership is narrowed too.** When an active set is declared, ownership-derived access is + honored only if it is reachable through the active roles. A stronger least-privilege guarantee, but a + larger behavioral change. +- **Option C — make it explicit.** A reserved value in the header grammar lets the caller opt ownership + in or out, mirroring Snowflake's primary-vs-secondary distinction. + +Proposed default: ship **Option A** for v1 — it delivers the read-narrowing that motivates the feature +without surprising existing users — and design the grammar so Option C can be layered on later without a +breaking change. + +--- + +## 6. Scope & limitations + +Narrowing governs Gravitino's **native authorization path** — the Iceberg REST catalog and the native +Gravitino API, which Gravitino enforces itself. It does **not** reach catalogs that delegate +authorization to an external system: Hive, JDBC, and Hadoop SQL push enforcement down to Ranger or a +JDBC authorization plugin, which evaluate against the mapped user and groups and never see the +active-role declaration. + +This proposal is scoped to the Iceberg REST path, so that boundary is consistent — but it should be +documented clearly so operators don't expect blanket coverage across every catalog type. + +--- + +## 7. Transport — how each engine sends the active role + +The mechanism is a request header. Engines differ only in whether they can forward it: + +| Access path | Works today? | What's needed | +|---|---|---| +| Direct Gravitino API | ✅ Yes | Server-side change only | +| **Spark** (via Iceberg REST) | ✅ Yes | No code change — a catalog config setting | +| **Trino** (via Iceberg REST) | ❌ No | A small connector setting, *or* the token path (no Trino change) | Review Comment: New version Trino starts to support configure custom HTTP header. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
