bharos commented on code in PR #12757:
URL: https://github.com/apache/gravitino/pull/12757#discussion_r4100632835


##########
design-docs/tag-based-access-control.md:
##########
@@ -0,0 +1,768 @@
+<!--
+  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 of Tag-Based Access Control in Gravitino
+
+**Status:** draft for discussion. The [open questions](#open-questions) are 
deliberately left
+undecided in this revision, each presented with its options; decisions will be 
folded in after
+review.
+
+Discussion: [#12619](https://github.com/apache/gravitino/discussions/12619)
+
+---
+
+## Summary
+
+An access rule is a `Policy` of type `system_access_control` whose `content` 
carries a set of
+privileges and a role condition. The policy is bound to a tag. Any object 
carrying that tag
+becomes subject to the rule.
+
+```json
+POST /api/metalakes/prod/policies
+{
+  "name": "certified_access",
+  "policyType": "system_access_control",
+  "enabled": true,
+  "content": {
+    "privileges": ["SELECT_TABLE", "MODIFY_TABLE"],
+    "applicableRoles": ["analyst", "data_engineer"]
+  }
+}
+```
+
+```
+PUT  /api/metalakes/prod/tags/certified/policies/certified_access
+     { "selector": { "type": "ALL_VALUES" } }
+
+POST /api/metalakes/prod/objects/TABLE/lakehouse.finance.orders/tags
+     { "tagsToAdd": [{ "name": "certified" }] }
+```
+
+Read together: *a caller holding either `analyst` or `data_engineer` may 
select from and modify
+any table that carries the tag `certified`.* Any listed role satisfies the 
condition, and every
+listed privilege is conferred to it.
+
+Only one thing is attached: the policy to the tag. The roles are values inside 
`content`, not a
+second link. No new user-facing entity, REST resource or client API is 
introduced.
+
+---
+
+## Background
+
+Gravitino authorizes metadata operations through RBAC. A grant names a 
securable object and a
+privilege and binds them to a role; the authorization expression on each REST 
endpoint evaluates
+those grants over the object's ancestor chain.
+
+Tags are a separate subsystem. They apply to catalogs, schemas, tables, views, 
topics, filesets,
+models, columns and functions, carry assignment values (see
+[tag-assignment-values.md](tag-assignment-values.md)), and inherit down the 
object hierarchy.
+Policy-on-tag ([policy-on-tag.md](policy-on-tag.md)) lets governance policies 
be selected by those
+tags. Authorization does not read tags at all.
+
+So a label cannot drive access. An organization that already tags tables 
`certified`, `pii` or
+`data_domain=finance` must still issue grants object by object to act on those 
tags. New objects
+need new grants, dropped objects leave stale ones, and the rule itself is 
written down nowhere — it
+exists only as the pile of grants someone remembered to issue.
+
+---
+
+## Scope
+
+### In this version
+
+- A rule of the form *(action, role condition)* bound to a tag.
+- `ALLOW` only.
+- Roles as the matched condition.
+- Evaluation inside the existing authorization-expression path, composing with 
RBAC.
+- Reuse of the `Policy` entity, the policy-to-tag relation and 
`PolicySelector`, so tag conditions
+  are written identically for governance and for authorization.
+- No new REST resource or client API. The only storage addition is an internal 
derived index, not
+  written or read by any endpoint — see [Lifecycle](#lifecycle).
+
+### Not in this version
+
+| Excluded                                                      | Reason       
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                       |
+| ------------------------------------------------------------- | 
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
+| `DENY`                                                        | A deny that 
a descendant tag cannot undo is a separate problem. Allow-only means every rule 
set has an answer and the order rules are applied in never matters.             
                                                                                
                                                                                
                                                                                
                                                        |
+| Users and groups as the matched condition                     | The 
condition schema can gain them later without changing the model.                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                |
+| Row filtering and column masking                              | Distinct 
policy types; this design governs whole-object decisions.                       
                                                                                
                                                                                
                                                                                
                                                                                
                                                           |
+| Cross-tag conditions                                          | A rule 
matches one tag. Conditions spanning several tags await the `EXPRESSION` 
selector type in [policy-on-tag.md](policy-on-tag.md).                          
                                                                                
                                                                                
                                                                                
                                                                    |
+| Column-level decisions                                        | A tag on a 
column does not affect decisions about its table.                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                         |
+| A `scope` field in `content`, restricting a rule to a subtree | Not a 
security boundary: creating a policy already needs metalake-wide 
`CREATE_POLICY`, so whoever writes the rule chooses its reach anyway. It is 
also not checked when a tag is applied, so it limits where a rule takes effect 
rather than stopping a wrong tag. One tag meaning different things in different 
subtrees is already covered by tag assignment values with a value-sensitive 
selector. Can be added later, since an absent `scope` has always meant 
metalake-wide. |
+| Replacing RBAC                                                | Baseline 
privileges, ownership and traversal are unchanged. See [Composition with 
RBAC](#composition-with-rbac).                                                  
                                                                                
                                                                                
                                                                                
                                                                  |
+
+---
+
+## Alternatives considered
+
+| Option                                                                 | 
Pros                                                                            
                                 | Cons                                         
                                                                                
| Status       |
+| ---------------------------------------------------------------------- | 
----------------------------------------------------------------------------------------------------------------
 | 
----------------------------------------------------------------------------------------------------------------------------
 | ------------ |
+| **A `system_access_control` policy type bound to a tag**               | 
Reuses the entity, relation, selector and resolver; no new REST or client 
surface; one governance model to learn | The role condition lives in `content` 
JSON, so lookup by role needs a derived index rather than a foreign key         
       | **Proposed** |
+| A dedicated `tag_access_policy` entity with action and role as columns | 
Foreign key on role; indexed lookup; cascade on role deletion falls out of the 
schema                            | New table across three dialects, new REST 
resource, new client and CLI surface, a second governance model alongside 
policies | Rejected     |
+| Extend RBAC grants with a tag predicate                                | No 
new concepts                                                                    
                              | The grant table is object-identified; a 
predicate has no object, and every grant read path would change                 
     | Rejected     |
+| Evaluate tags in an external engine (OPA and similar)                  | 
Arbitrary policy language                                                       
                                 | Moves the decision out of Gravitino, 
duplicates the tag hierarchy, and cannot use the existing expression path       
        | Rejected     |
+
+That one con means the server keeps the role reference consistent, rather than 
the database schema
+doing it. [Lifecycle](#lifecycle) covers how.
+
+---
+
+## Model
+
+### Content
+
+`PolicyContent` is an interface, and each built-in policy type has a concrete 
implementation with
+typed fields and a `validate()` that runs at write time. 
`IcebergDataCompactionContent` is the
+existing example. `system_access_control` follows the same pattern with a new
+`AccessControlContent`:
+
+| Field              | Type                     | Meaning                      
                                                                                
       |
+| ------------------ | ------------------------ | 
-------------------------------------------------------------------------------------------------------------------
 |
+| `privileges`       | list of `Privilege.Name` | The privileges the rule 
confers. Each must be a permitted name — see [Permitted 
privileges](#permitted-privileges). |
+| `applicableRoles`  | list of role names       | The **condition**. Satisfied 
when any listed role is among the caller's expanded roles.                      
       |
+
+`validate()` rejects at creation rather than at evaluation:
+
+- `privileges` is non-empty and every entry parses to a permitted 
`Privilege.Name`.
+- `applicableRoles` is non-empty and every name is non-blank.
+
+Rejecting at write time matters because the alternative failure is silent: a 
policy naming a
+privilege that does not parse simply grants nothing, and nothing surfaces 
until someone notices
+the access they expected is missing.
+
+Whether `validate()` also requires the named role to *exist* is part of
+[OQ-3](#oq-3--deleting-a-referenced-role), not a separate decision.
+
+### Permitted privileges
+
+Parsing to a `Privilege.Name` is a syntax check, not a safety one. A rule may 
confer only
+privileges that grant access to the tagged object itself, so `validate()` 
checks each name against
+a fixed allowlist:
+
+| Object   | Permitted                                          |
+| -------- | -------------------------------------------------- |
+| Table    | `SELECT_TABLE`, `MODIFY_TABLE`, `PROBE_TABLE_LIKE` |
+| View     | `SELECT_VIEW`                                      |
+| Fileset  | `READ_FILESET`, `WRITE_FILESET`                    |
+| Topic    | `CONSUME_TOPIC`, `PRODUCE_TOPIC`                   |
+| Model    | `USE_MODEL`                                        |
+| Function | `EXECUTE_FUNCTION`                                 |
+
+An allowlist rather than a denylist so the boundary fails closed: a privilege 
added to
+`Privilege.Name` later confers nothing through a tag until someone adds it 
here deliberately.
+
+Everything else is rejected. Two classes are worth naming because the reasons 
differ:
+
+- **Authority over the authorization system** — `MANAGE_USERS`, 
`MANAGE_GROUPS`, `MANAGE_GRANTS`,
+  `CREATE_ROLE`, `CREATE_TAG`, `APPLY_TAG`, `CREATE_POLICY`, `APPLY_POLICY`. 
These turn one tagging
+  operation into a standing ability to widen access. `MANAGE_GRANTS` binds to 
every taggable type
+  and covers all children of whatever it binds to, so a tag carrying it on one 
catalog would let
+  every role in `applicableRoles` grant anything beneath that catalog — and 
would keep doing so
+  after the applier's own authority was revoked. `APPLY_TAG` and 
`APPLY_POLICY` close the loop
+  further, letting a conferred role extend the tag system's own reach.
+- **Traversal** — `USE_CATALOG` and `USE_SCHEMA`, for the reasons in
+  [Traversal stays RBAC](#traversal-stays-rbac).
+
+Both exclusions are the same argument: a tag confers access to data, never the 
ability to hand out
+access or to reach new territory. 
[OQ-4](#oq-4--authority-to-confer-access-through-a-tag) governs
+who may apply a rule and does not substitute for this, because the applier 
holds the authority
+privilege by construction — the check they pass is exactly the one the rule 
would make permanent.
+
+### `applicableRoles` is a condition, not a principal
+
+The rule does not grant anything to `analyst`. It states that *if* the caller 
holds `analyst`
+among their expanded roles *and* the object carries `certified`, then 
`SELECT_TABLE` is satisfied
+for this request.
+
+The distinction matters for two reasons. The rule is not a grant, so it does 
not appear in the
+role's securable objects and does not participate in grant listing. And a role 
that is never
+assigned to anyone confers nothing, exactly as an unassigned role does today.
+
+### The tag bind
+
+The policy is attached to the tag through the existing policy-to-tag relation 
and its selector,
+exactly as governance policies are. `ALL_VALUES` in the example above matches 
the tag regardless of
+assignment value; value-sensitive selectors work as they do for governance 
policies, and nothing in
+this design is specific to `ALL_VALUES`.
+
+---
+
+## Evaluation
+
+An authorization decision needs to know, for the object being accessed and the 
caller's roles,
+whether any access rule is satisfied. That requires three things:
+
+1. the tags effective at the object after nearest-wins resolution
+   ([tag-assignment-values.md](tag-assignment-values.md)), including those 
inherited from ancestors;
+2. the `system_access_control` policies bound to those tags;
+3. for each, whether any of `applicableRoles` is among the caller's expanded 
roles.

Review Comment:
   The proposed option below states this part. The evaluation is like
   ```
   (rbac_allow || tag_allow) && !rbac_deny 
   ```
   
   So if privileges are defined both through RBAC and tag, then it's 
essentially a union of both.
   DENY only comes from RBAC (we are not supporting tag based deny as of now); 
so this can overpower any ALLOW.



##########
design-docs/tag-based-access-control.md:
##########
@@ -0,0 +1,768 @@
+<!--
+  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 of Tag-Based Access Control in Gravitino
+
+**Status:** draft for discussion. The [open questions](#open-questions) are 
deliberately left
+undecided in this revision, each presented with its options; decisions will be 
folded in after
+review.
+
+Discussion: [#12619](https://github.com/apache/gravitino/discussions/12619)
+
+---
+
+## Summary
+
+An access rule is a `Policy` of type `system_access_control` whose `content` 
carries a set of
+privileges and a role condition. The policy is bound to a tag. Any object 
carrying that tag
+becomes subject to the rule.
+
+```json
+POST /api/metalakes/prod/policies
+{
+  "name": "certified_access",
+  "policyType": "system_access_control",
+  "enabled": true,
+  "content": {
+    "privileges": ["SELECT_TABLE", "MODIFY_TABLE"],
+    "applicableRoles": ["analyst", "data_engineer"]
+  }
+}
+```
+
+```
+PUT  /api/metalakes/prod/tags/certified/policies/certified_access
+     { "selector": { "type": "ALL_VALUES" } }
+
+POST /api/metalakes/prod/objects/TABLE/lakehouse.finance.orders/tags
+     { "tagsToAdd": [{ "name": "certified" }] }
+```
+
+Read together: *a caller holding either `analyst` or `data_engineer` may 
select from and modify
+any table that carries the tag `certified`.* Any listed role satisfies the 
condition, and every
+listed privilege is conferred to it.
+
+Only one thing is attached: the policy to the tag. The roles are values inside 
`content`, not a
+second link. No new user-facing entity, REST resource or client API is 
introduced.
+
+---
+
+## Background
+
+Gravitino authorizes metadata operations through RBAC. A grant names a 
securable object and a
+privilege and binds them to a role; the authorization expression on each REST 
endpoint evaluates
+those grants over the object's ancestor chain.
+
+Tags are a separate subsystem. They apply to catalogs, schemas, tables, views, 
topics, filesets,
+models, columns and functions, carry assignment values (see
+[tag-assignment-values.md](tag-assignment-values.md)), and inherit down the 
object hierarchy.
+Policy-on-tag ([policy-on-tag.md](policy-on-tag.md)) lets governance policies 
be selected by those
+tags. Authorization does not read tags at all.
+
+So a label cannot drive access. An organization that already tags tables 
`certified`, `pii` or
+`data_domain=finance` must still issue grants object by object to act on those 
tags. New objects
+need new grants, dropped objects leave stale ones, and the rule itself is 
written down nowhere — it
+exists only as the pile of grants someone remembered to issue.
+
+---
+
+## Scope
+
+### In this version
+
+- A rule of the form *(action, role condition)* bound to a tag.
+- `ALLOW` only.
+- Roles as the matched condition.
+- Evaluation inside the existing authorization-expression path, composing with 
RBAC.
+- Reuse of the `Policy` entity, the policy-to-tag relation and 
`PolicySelector`, so tag conditions
+  are written identically for governance and for authorization.
+- No new REST resource or client API. The only storage addition is an internal 
derived index, not
+  written or read by any endpoint — see [Lifecycle](#lifecycle).
+
+### Not in this version
+
+| Excluded                                                      | Reason       
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                       |
+| ------------------------------------------------------------- | 
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
+| `DENY`                                                        | A deny that 
a descendant tag cannot undo is a separate problem. Allow-only means every rule 
set has an answer and the order rules are applied in never matters.             
                                                                                
                                                                                
                                                                                
                                                        |
+| Users and groups as the matched condition                     | The 
condition schema can gain them later without changing the model.                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                |
+| Row filtering and column masking                              | Distinct 
policy types; this design governs whole-object decisions.                       
                                                                                
                                                                                
                                                                                
                                                                                
                                                           |
+| Cross-tag conditions                                          | A rule 
matches one tag. Conditions spanning several tags await the `EXPRESSION` 
selector type in [policy-on-tag.md](policy-on-tag.md).                          
                                                                                
                                                                                
                                                                                
                                                                    |
+| Column-level decisions                                        | A tag on a 
column does not affect decisions about its table.                               
                                                                                
                                                                                
                                                                                
                                                                                
                                                         |
+| A `scope` field in `content`, restricting a rule to a subtree | Not a 
security boundary: creating a policy already needs metalake-wide 
`CREATE_POLICY`, so whoever writes the rule chooses its reach anyway. It is 
also not checked when a tag is applied, so it limits where a rule takes effect 
rather than stopping a wrong tag. One tag meaning different things in different 
subtrees is already covered by tag assignment values with a value-sensitive 
selector. Can be added later, since an absent `scope` has always meant 
metalake-wide. |
+| Replacing RBAC                                                | Baseline 
privileges, ownership and traversal are unchanged. See [Composition with 
RBAC](#composition-with-rbac).                                                  
                                                                                
                                                                                
                                                                                
                                                                  |
+
+---
+
+## Alternatives considered
+
+| Option                                                                 | 
Pros                                                                            
                                 | Cons                                         
                                                                                
| Status       |
+| ---------------------------------------------------------------------- | 
----------------------------------------------------------------------------------------------------------------
 | 
----------------------------------------------------------------------------------------------------------------------------
 | ------------ |
+| **A `system_access_control` policy type bound to a tag**               | 
Reuses the entity, relation, selector and resolver; no new REST or client 
surface; one governance model to learn | The role condition lives in `content` 
JSON, so lookup by role needs a derived index rather than a foreign key         
       | **Proposed** |
+| A dedicated `tag_access_policy` entity with action and role as columns | 
Foreign key on role; indexed lookup; cascade on role deletion falls out of the 
schema                            | New table across three dialects, new REST 
resource, new client and CLI surface, a second governance model alongside 
policies | Rejected     |
+| Extend RBAC grants with a tag predicate                                | No 
new concepts                                                                    
                              | The grant table is object-identified; a 
predicate has no object, and every grant read path would change                 
     | Rejected     |
+| Evaluate tags in an external engine (OPA and similar)                  | 
Arbitrary policy language                                                       
                                 | Moves the decision out of Gravitino, 
duplicates the tag hierarchy, and cannot use the existing expression path       
        | Rejected     |
+
+That one con means the server keeps the role reference consistent, rather than 
the database schema
+doing it. [Lifecycle](#lifecycle) covers how.
+
+---
+
+## Model
+
+### Content
+
+`PolicyContent` is an interface, and each built-in policy type has a concrete 
implementation with
+typed fields and a `validate()` that runs at write time. 
`IcebergDataCompactionContent` is the
+existing example. `system_access_control` follows the same pattern with a new
+`AccessControlContent`:
+
+| Field              | Type                     | Meaning                      
                                                                                
       |
+| ------------------ | ------------------------ | 
-------------------------------------------------------------------------------------------------------------------
 |
+| `privileges`       | list of `Privilege.Name` | The privileges the rule 
confers. Each must be a permitted name — see [Permitted 
privileges](#permitted-privileges). |
+| `applicableRoles`  | list of role names       | The **condition**. Satisfied 
when any listed role is among the caller's expanded roles.                      
       |
+
+`validate()` rejects at creation rather than at evaluation:
+
+- `privileges` is non-empty and every entry parses to a permitted 
`Privilege.Name`.
+- `applicableRoles` is non-empty and every name is non-blank.
+
+Rejecting at write time matters because the alternative failure is silent: a 
policy naming a
+privilege that does not parse simply grants nothing, and nothing surfaces 
until someone notices
+the access they expected is missing.
+
+Whether `validate()` also requires the named role to *exist* is part of
+[OQ-3](#oq-3--deleting-a-referenced-role), not a separate decision.
+
+### Permitted privileges
+
+Parsing to a `Privilege.Name` is a syntax check, not a safety one. A rule may 
confer only
+privileges that grant access to the tagged object itself, so `validate()` 
checks each name against
+a fixed allowlist:
+
+| Object   | Permitted                                          |
+| -------- | -------------------------------------------------- |
+| Table    | `SELECT_TABLE`, `MODIFY_TABLE`, `PROBE_TABLE_LIKE` |
+| View     | `SELECT_VIEW`                                      |
+| Fileset  | `READ_FILESET`, `WRITE_FILESET`                    |
+| Topic    | `CONSUME_TOPIC`, `PRODUCE_TOPIC`                   |
+| Model    | `USE_MODEL`                                        |
+| Function | `EXECUTE_FUNCTION`                                 |
+
+An allowlist rather than a denylist so the boundary fails closed: a privilege 
added to
+`Privilege.Name` later confers nothing through a tag until someone adds it 
here deliberately.
+
+Everything else is rejected. Two classes are worth naming because the reasons 
differ:
+
+- **Authority over the authorization system** — `MANAGE_USERS`, 
`MANAGE_GROUPS`, `MANAGE_GRANTS`,
+  `CREATE_ROLE`, `CREATE_TAG`, `APPLY_TAG`, `CREATE_POLICY`, `APPLY_POLICY`. 
These turn one tagging
+  operation into a standing ability to widen access. `MANAGE_GRANTS` binds to 
every taggable type
+  and covers all children of whatever it binds to, so a tag carrying it on one 
catalog would let
+  every role in `applicableRoles` grant anything beneath that catalog — and 
would keep doing so
+  after the applier's own authority was revoked. `APPLY_TAG` and 
`APPLY_POLICY` close the loop
+  further, letting a conferred role extend the tag system's own reach.
+- **Traversal** — `USE_CATALOG` and `USE_SCHEMA`, for the reasons in
+  [Traversal stays RBAC](#traversal-stays-rbac).
+
+Both exclusions are the same argument: a tag confers access to data, never the 
ability to hand out
+access or to reach new territory. 
[OQ-4](#oq-4--authority-to-confer-access-through-a-tag) governs
+who may apply a rule and does not substitute for this, because the applier 
holds the authority
+privilege by construction — the check they pass is exactly the one the rule 
would make permanent.
+
+### `applicableRoles` is a condition, not a principal
+
+The rule does not grant anything to `analyst`. It states that *if* the caller 
holds `analyst`
+among their expanded roles *and* the object carries `certified`, then 
`SELECT_TABLE` is satisfied
+for this request.
+
+The distinction matters for two reasons. The rule is not a grant, so it does 
not appear in the
+role's securable objects and does not participate in grant listing. And a role 
that is never
+assigned to anyone confers nothing, exactly as an unassigned role does today.
+
+### The tag bind
+
+The policy is attached to the tag through the existing policy-to-tag relation 
and its selector,
+exactly as governance policies are. `ALL_VALUES` in the example above matches 
the tag regardless of
+assignment value; value-sensitive selectors work as they do for governance 
policies, and nothing in
+this design is specific to `ALL_VALUES`.
+
+---
+
+## Evaluation
+
+An authorization decision needs to know, for the object being accessed and the 
caller's roles,
+whether any access rule is satisfied. That requires three things:
+
+1. the tags effective at the object after nearest-wins resolution
+   ([tag-assignment-values.md](tag-assignment-values.md)), including those 
inherited from ancestors;
+2. the `system_access_control` policies bound to those tags;
+3. for each, whether any of `applicableRoles` is among the caller's expanded 
roles.
+
+Access rules only allow — `content` has a role condition but no deny effect — 
so a tag cannot
+restrict or deny. An RBAC `DENY` is unaffected; see [Allow and 
deny](#allow-and-deny).
+
+The question is *where* steps 1 and 2 happen.
+
+### Proposed: check tags at the privilege leaf
+
+Every privilege check bottoms out in `GravitinoAuthorizer.authorize`. The 
expression converter
+expands each `ANY_*` macro mechanically —
+
+```
+ANY_USE_CATALOG → ANY(USE_CATALOG, METALAKE, CATALOG) && 
!ANY(DENY_USE_CATALOG, METALAKE, CATALOG)
+```
+
+— and `hasAuthorizeWithoutDeny` walks the object's ancestor chain calling 
`authorize` and `deny` at
+each level. Tag evaluation goes inside `authorize`: when the RBAC rows do not 
allow, resolve the
+effective tags of the object being decided, load the access policies bound to 
them, and test those
+against the caller's roles.
+
+Three properties follow from the surrounding code rather than from a rule this 
design has to write:
+
+- **RBAC deny still wins.** The `!ANY(DENY_…)` conjunct is built from `deny`, 
which the tag path
+  never touches, so an allow-only tag cannot reach it. See
+  [OQ-2](#oq-2--composition-when-a-tag-allows-and-rbac-denies).
+- **Traversal stays RBAC.** Each conjunct consults tags independently, so a 
tag granting
+  `SELECT_TABLE` still cannot bypass `USE_CATALOG`.
+- **A denial stays attributable.** The tag check is a distinct step, so the 
information needed to
+  explain a decision stays separable from the grant that would otherwise have 
produced it.
+
+One rule does not come for free. Role assumption narrows a request to the 
roles the caller
+activated, but that narrowing lives inside `enforceNarrowed`, on the jCasbin 
path the tag check
+does not take. The tag check therefore applies it itself: `applicableRoles` is 
tested against the
+caller's *active* roles, and an `ActiveRoles.none()` request grants nothing. 
Otherwise a caller who
+narrowed would silently keep tag-derived access they had asked to drop.
+
+Inheritance is the one thing the walk does not supply. `authorize` resolves 
the object's effective
+tags once ([tag-assignment-values.md](tag-assignment-values.md)) rather than 
asking each level in
+turn. Different tag names still union down the chain; nearest-wins settles 
only the same name
+assigned at two levels, where the nearer assignment wins and the farther one 
is dropped:
+
+```
+catalog lakehouse        certified = gold      pii = true
+table   finance.orders   certified = bronze
+
+effective on the table   certified = bronze    pii = true
+```
+
+`pii` is inherited; `certified=gold` is gone because the table overrode it. So 
a rule bound with
+`TAG_VALUE("gold")` does not match, while asking level by level would still 
find `gold` on the
+catalog and grant. The two readings agree under `ALL_VALUES`, where only the 
presence of the name
+matters, and diverge as soon as a rule reads the value.
+
+One constraint on where the check hooks in. `hasAuthorizeWithoutDeny` walks 
the ancestor chain
+calling `authorize` at each level, so a check placed inside that per-level 
call would resolve
+effective tags once per level, each resolution walking its own chain — 
quadratic in chain depth,
+and for nothing, since the leaf's effective tags already subsume every 
ancestor's. The check runs
+once for the object under decision, not once per level of the RBAC walk.
+
+The cost lands on the request path, and is set out in [Cost](#cost).
+
+### Decision flow
+
+The following flow describes an ordinary tag-conferrable privilege check. 
Existing ownership
+branches and the enclosing authorization expression remain responsible for the 
complete endpoint
+decision, including RBAC-only traversal checks.

Review Comment:
   Yes - a server-level config, default off. This is already documented in the 
"Enabling the feature" section (also tidied up in latest commit)
   
   And you're right that it's already a no-op without one: a metalake with no 
system_access_control policy confers nothing. The flag is a separate kill 
switch on the authorization path, so an operator can turn tag evaluation off 
without unbinding policies one at a time.
   
   With it off, policies and tags can still be created, bound and applied - 
only the authorizer stops reading them. 



-- 
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]

Reply via email to