yuqi1129 commented on code in PR #13867:
URL: https://github.com/apache/gravitino/pull/13867#discussion_r4283701196


##########
design-docs/domain-design.md:
##########
@@ -0,0 +1,261 @@
+<!--
+  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: Domain
+
+| Version | Author | Date | Status |
+| :---- | :---- | :---- | :---- |
+| 1.23 | Jerry Shao | 2026-10-15 | Draft |
+
+## Goals
+
+1. **Business grouping**: group assets from any catalog under named domains 
(e.g. "Finance") for discovery, browsing and agent retrieval. An asset can 
belong to several domains.
+2. **Membership is tags**: an asset is a member when it carries the domain's 
backing tag, directly or inherited from its catalog or schema. There is no 
separate membership store.
+3. **Separate privileges**: domain privileges govern the `Domain` object; tag 
privileges govern membership. Membership grants no access to data.
+4. **One level of subdomains**, each with one parent; a subdomain can be moved.
+5. **Draft and published status**: a `DRAFT` domain is visible only to its 
owner and managers, the same on REST, MCP and any UI.
+6. **Inherited membership**: tagging a catalog or schema makes the objects 
under it members, through Gravitino's existing tag inheritance.
+
+## Non-Goals
+
+1. **Approval workflow**: adding members, attaching subdomains and publishing 
take effect at once; status is a manual switch.
+2. **Data-access grants on domains**: a domain carries no data privileges such 
as `SELECT_TABLE` (see Grants and policies).
+3. **Deeper or multi-parent hierarchies**: the storage allows them later 
without a schema change.
+4. **Automatic membership**, glossary and metric designs, and the ABAC policy 
language. Glossary terms can become members once they are taggable and added to 
the member types.
+5. **UI**: this design covers the server, REST APIs, clients and MCP tools.
+
+## Usage Scenario
+
+1. A steward with `MANAGE_DOMAIN` and `CREATE_TAG` on the metalake creates 
domain Finance. Gravitino creates tag `Finance` for it; the steward owns both.
+2. The steward creates Trading under Finance (`"parent": "Finance"`); it gets 
tag `Finance/Trading`. The steward grants `APPLY_TAG` on that tag to the 
trading table owners.
+3. A table owner tags `cat.db.trades` with `Finance/Trading`. The table is in 
Trading, and appears under Finance through the subdomain roll-up. Tagging 
schema `cat.db` with `Finance` would put every table in it in Finance too.
+4. A user or agent who can see Trading browses it and gets the tables they can 
access.
+5. Trading is moved under Markets. Its members now appear under Markets; 
nothing is retagged and the tag keeps its name.
+
+## Design
+
+### Data model
+
+```
+Domain    { id, name, comment, status: DRAFT | PUBLISHED, properties, 
audit_info }
+DomainRel { parentId, childId, audit_info }      // one edge per subdomain
+```
+
+- A unique index on `DomainRel (child_id, deleted_at)` keeps one parent per 
domain. `parent` is not stored on `Domain`; the API derives it. Dropping the 
index and checks would allow multiple parents later without a schema change.
+- Domains live under a metalake. A subdomain and its parent are in the same 
metalake.
+- Domain names are unique within a metalake across both levels: top-level 
domains and subdomains share one namespace, so a domain is addressed by its 
name alone.
+
+### Backing tag
+
+Each domain has **at most one** backing tag: a tag, optionally with a value 
(tag values: apache/gravitino#12162). Creating a domain always sets one; a 
domain has none only after its backing tag is removed or deleted, until a new 
one is set.
+
+- **Default**: if none is given at creation, Gravitino creates a `NO_VALUE` 
tag named after the domain's path, as in Unity Catalog: `Finance`, and 
`Finance/Tax` for subdomain Tax. The creator owns it, so creating a domain this 
way also needs `CREATE_TAG`. If a tag with that name already exists, the domain 
adopts it, with the assets that carry it, only when the tag backs no other 
domain and the caller owns the tag or the metalake. If the tag already backs 
another domain, creation fails with 400; if the caller may not adopt it, with 
403.
+- **Name clashes**: default tag names can clash even without `/` in domain 
names, because tags are never renamed on move or rename. For example, rename 
subdomain Tax under Finance to VAT (its tag stays `Finance/Tax`), then create a 
new Tax under Finance: both want `Finance/Tax`. Banning `/` would not prevent 
this, so domain names may contain `/`, and the adoption rule above resolves 
clashes.
+- **Matching** follows the tag lookup semantics (`GET /tags/{tag}/objects`). A 
backing tag without a value matches every assignment of the tag, with or 
without values, like a lookup without `value`. A backing tag with a value 
matches assignments whose values contain it, exact and case-sensitive, like 
`?value=`; a valueless assignment never matches it. The same rule applies to 
member listing, domains of an object and domain filters, always against the 
object's effective assignment, and to a default-name tag adopted at creation, 
which is bound without a value.
+- **Explicit**: any existing (tag, value), such as `dept=finance`, bound by 
that tag's owner (or the metalake owner). A (tag, value) may be bound 
explicitly to several domains, which then have the same members.
+- **Storage**: the backing tag is the `DOMAIN` object's own row in 
`tag_relation_meta`; there is no extra field to keep in sync. Reverse lookup 
uses `idx_tid_value` filtered to `DOMAIN` objects. Domains never inherit tags 
(`METALAKE` is not taggable).
+- **Changing it**: `PUT /domains/{domain}/backingTag` replaces or removes it 
on the server, under the domain's row lock. A missing tag fails with 404, a 
value the tag does not allow with 400, and a tag the caller does not own (nor 
the metalake) with 403; setting the current tag is a no-op. Assets carrying 
only the old tag drop out; nothing is migrated.
+- **Not through the generic tag association API**: it rejects `DOMAIN` 
targets, so the endpoint above is the only way to change a backing tag. Through 
the generic API a missing tag is silently ignored, so replacing the tag with a 
misspelled one would strip the backing tag and all members without an error; 
the call returns tag names instead of the domain; and `tagsToRemove` would come 
from a possibly stale snapshot.
+- **Never renamed**: moving or renaming a domain does not rename its tag; the 
hierarchy lives in `DomainRel`.
+- **Deleting the tag** is allowed, as for any tag; its domains are left with 
no backing tag and no members until a new one is set. There is no domain-side 
audit of this.
+- **Tag object listings**: a backing tag is a tag association of a `DOMAIN` 
object, so `GET /tags/{tag}/objects` and `Tag#associatedObjects` include the 
domains the tag backs, filtered by the domain visibility rule (see Status). A 
tag owner can then see which domains the tag backs.
+
+### Subdomains
+
+Every operation that sets a parent (create with `parent`, attach, move) checks:
+
+| # | Constraint |
+| :---- | :---- |
+| C1 | A domain cannot be its own parent |
+| C2 | The parent is top-level |
+| C3 | The attached domain has no subdomains |
+| C4 | Parent and child are in the same metalake |
+| C5 | At most one parent; attaching a domain that has one is a move |
+| C6 | A `PUBLISHED` domain can only be attached under a `PUBLISHED` parent |
+
+C1–C3 keep the hierarchy one level deep and acyclic, so no cycle detection is 
needed.
+
+- **Attach / move** re-points the child's single edge in one transaction. 
Attaching to the current parent is a no-op.
+- **Detach** removes the edge; the child becomes top-level.
+- **Locking**: each operation locks the domain rows involved (child, new 
parent, old parent) in id order, then checks the constraints. Otherwise two 
valid calls could together create two levels, a cycle, or a dangling edge.
+- **Delete** is refused while the domain has subdomains. Deleting removes the 
domain's own parent edge and its backing tag association. The tag and the tags 
on assets stay.
+- **Membership and scope**: membership comes from backing tags only (see 
Membership and inheritance), so an asset tagged only `Finance/Trading` is a 
member of Trading, not of Finance. A top-level domain's *scope* is its own 
members plus the members of the subdomains the caller can see; a subdomain's 
scope is its own members. Member listing and domain filters use scope, as in 
Unity Catalog, where a subdomain's assets also appear under its parent domain.

Review Comment:
   **Membership vs scope: one definition per consumer**
   
   Thanks for 394f9be7cb / fbe71ab048. Splitting *membership* from *scope* and 
expanding listings to leaf objects fixes most of what I wanted to raise here. 
Below is the frame I used, so the remaining gaps are easy to see.
   
   An object can relate to Finance through three independent paths:
   
   | Path | Example | Hierarchy it follows |
   | :---- | :---- | :---- |
   | ① direct | `cat.db.t` carries `Finance` | none |
   | ② inherited | schema `cat.db` carries `Finance`, so `cat.db.a` is in 
Finance | catalog → schema → leaf |
   | ③ roll-up | `cat.other.x` carries `Finance/Trading`, and Trading is under 
Finance | domain → subdomain |
   
   In the current text, *membership* = ① + ② (leaf objects only) and *scope* = 
membership + ③ over visible subdomains. My concern was that every consumer 
picked its own combination. Here is where each consumer stands now:
   
   | Consumer | Uses | Status |
   | :---- | :---- | :---- |
   | Member listing | scope, expanded to leaves | ✅ defined |
   | Domain filters | scope | ✅ defined |
   | Domains of a leaf object | membership | ✅ defined (`Finance › Trading` 
from `parent`) |
   | Domains of a catalog/schema | "carries the tag" | ⚠️ a third relation, see 
3 |
   | Policies (L170) | membership, implicitly (tag-based) | ⚠️ not stated, see 
1 |
   | Counts (L129) | "consistent with the listing" | ⚠️ ambiguous, see 2 |
   
   Remaining points:
   
   1. **Policies follow membership, not scope. Please say so explicitly here.** 
With `cat.other.x` tagged only `Finance/Trading`, the Finance page lists 
`cat.other.x` (scope), but Finance's policies do not apply to it (membership). 
Users browsing Finance will assume they do. One sentence in this bullet, 
stating which concepts use scope (browse, filter, count?) and which use 
membership (policies, domains of an object), would make that boundary a 
decision rather than an accident. Open Question 1 can then stay focused on 
whether to change it.
   
   2. **Counts.** "Consistent with the listing" means scope, and the listing 
depends on the caller: visible subdomains (drafts) and `filterMetadataObject`. 
So a domain's "asset count" is per viewer, not a property of the domain. If 
that is intended, please write it down: a UI card showing "Finance: 1,204 
assets" will show different numbers to different people. Otherwise, define the 
count over membership or over the unfiltered scope.
   
   3. **Domains of an object on a catalog or schema** return domains the 
container *carries* through the same response shape that, for a leaf, means *is 
a member of*. A client cannot tell the two apart from the DTO without checking 
the object type itself. Consider either not supporting this endpoint on 
catalogs and schemas, or adding an explicit marker on each entry (e.g. 
`relation: MEMBER | CARRIES`).
   
   4. **Store-only expansion (L127) creates a second source of truth.** For a 
table in an external catalog that Gravitino has not recorded yet, the Rule 
(L111) says it is a member through the catalog's tag, but the listing omits it. 
Please state which one is authoritative, e.g. "the listing is a view of 
recorded objects; membership is defined by the Rule". Then MCP and agent 
callers know that an empty listing does not mean "not in Finance".
   
   5. **Cost of steps 2–3.** As written, expansion followed by a per-candidate 
effective-tag check is N × (leaf + schema + catalog relation reads) for a 
catalog with N leaves. All leaves in a schema share the schema's and catalog's 
tags, so one batched read of the leaves' own tags per schema is enough. It 
would help to note that batching here, and for domain filters (L116), which 
also check each candidate.
   



##########
design-docs/domain-design.md:
##########
@@ -0,0 +1,261 @@
+<!--
+  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: Domain
+
+| Version | Author | Date | Status |
+| :---- | :---- | :---- | :---- |
+| 1.23 | Jerry Shao | 2026-10-15 | Draft |
+
+## Goals
+
+1. **Business grouping**: group assets from any catalog under named domains 
(e.g. "Finance") for discovery, browsing and agent retrieval. An asset can 
belong to several domains.
+2. **Membership is tags**: an asset is a member when it carries the domain's 
backing tag, directly or inherited from its catalog or schema. There is no 
separate membership store.
+3. **Separate privileges**: domain privileges govern the `Domain` object; tag 
privileges govern membership. Membership grants no access to data.
+4. **One level of subdomains**, each with one parent; a subdomain can be moved.
+5. **Draft and published status**: a `DRAFT` domain is visible only to its 
owner and managers, the same on REST, MCP and any UI.
+6. **Inherited membership**: tagging a catalog or schema makes the objects 
under it members, through Gravitino's existing tag inheritance.
+
+## Non-Goals
+
+1. **Approval workflow**: adding members, attaching subdomains and publishing 
take effect at once; status is a manual switch.
+2. **Data-access grants on domains**: a domain carries no data privileges such 
as `SELECT_TABLE` (see Grants and policies).
+3. **Deeper or multi-parent hierarchies**: the storage allows them later 
without a schema change.
+4. **Automatic membership**, glossary and metric designs, and the ABAC policy 
language. Glossary terms can become members once they are taggable and added to 
the member types.
+5. **UI**: this design covers the server, REST APIs, clients and MCP tools.
+
+## Usage Scenario
+
+1. A steward with `MANAGE_DOMAIN` and `CREATE_TAG` on the metalake creates 
domain Finance. Gravitino creates tag `Finance` for it; the steward owns both.
+2. The steward creates Trading under Finance (`"parent": "Finance"`); it gets 
tag `Finance/Trading`. The steward grants `APPLY_TAG` on that tag to the 
trading table owners.
+3. A table owner tags `cat.db.trades` with `Finance/Trading`. The table is in 
Trading, and appears under Finance through the subdomain roll-up. Tagging 
schema `cat.db` with `Finance` would put every table in it in Finance too.
+4. A user or agent who can see Trading browses it and gets the tables they can 
access.
+5. Trading is moved under Markets. Its members now appear under Markets; 
nothing is retagged and the tag keeps its name.
+
+## Design
+
+### Data model
+
+```
+Domain    { id, name, comment, status: DRAFT | PUBLISHED, properties, 
audit_info }
+DomainRel { parentId, childId, audit_info }      // one edge per subdomain
+```
+
+- A unique index on `DomainRel (child_id, deleted_at)` keeps one parent per 
domain. `parent` is not stored on `Domain`; the API derives it. Dropping the 
index and checks would allow multiple parents later without a schema change.
+- Domains live under a metalake. A subdomain and its parent are in the same 
metalake.
+- Domain names are unique within a metalake across both levels: top-level 
domains and subdomains share one namespace, so a domain is addressed by its 
name alone.
+
+### Backing tag
+
+Each domain has **at most one** backing tag: a tag, optionally with a value 
(tag values: apache/gravitino#12162). Creating a domain always sets one; a 
domain has none only after its backing tag is removed or deleted, until a new 
one is set.
+
+- **Default**: if none is given at creation, Gravitino creates a `NO_VALUE` 
tag named after the domain's path, as in Unity Catalog: `Finance`, and 
`Finance/Tax` for subdomain Tax. The creator owns it, so creating a domain this 
way also needs `CREATE_TAG`. If a tag with that name already exists, the domain 
adopts it, with the assets that carry it, only when the tag backs no other 
domain and the caller owns the tag or the metalake. If the tag already backs 
another domain, creation fails with 400; if the caller may not adopt it, with 
403.
+- **Name clashes**: default tag names can clash even without `/` in domain 
names, because tags are never renamed on move or rename. For example, rename 
subdomain Tax under Finance to VAT (its tag stays `Finance/Tax`), then create a 
new Tax under Finance: both want `Finance/Tax`. Banning `/` would not prevent 
this, so domain names may contain `/`, and the adoption rule above resolves 
clashes.
+- **Matching** follows the tag lookup semantics (`GET /tags/{tag}/objects`). A 
backing tag without a value matches every assignment of the tag, with or 
without values, like a lookup without `value`. A backing tag with a value 
matches assignments whose values contain it, exact and case-sensitive, like 
`?value=`; a valueless assignment never matches it. The same rule applies to 
member listing, domains of an object and domain filters, always against the 
object's effective assignment, and to a default-name tag adopted at creation, 
which is bound without a value.
+- **Explicit**: any existing (tag, value), such as `dept=finance`, bound by 
that tag's owner (or the metalake owner). A (tag, value) may be bound 
explicitly to several domains, which then have the same members.
+- **Storage**: the backing tag is the `DOMAIN` object's own row in 
`tag_relation_meta`; there is no extra field to keep in sync. Reverse lookup 
uses `idx_tid_value` filtered to `DOMAIN` objects. Domains never inherit tags 
(`METALAKE` is not taggable).
+- **Changing it**: `PUT /domains/{domain}/backingTag` replaces or removes it 
on the server, under the domain's row lock. A missing tag fails with 404, a 
value the tag does not allow with 400, and a tag the caller does not own (nor 
the metalake) with 403; setting the current tag is a no-op. Assets carrying 
only the old tag drop out; nothing is migrated.
+- **Not through the generic tag association API**: it rejects `DOMAIN` 
targets, so the endpoint above is the only way to change a backing tag. Through 
the generic API a missing tag is silently ignored, so replacing the tag with a 
misspelled one would strip the backing tag and all members without an error; 
the call returns tag names instead of the domain; and `tagsToRemove` would come 
from a possibly stale snapshot.
+- **Never renamed**: moving or renaming a domain does not rename its tag; the 
hierarchy lives in `DomainRel`.
+- **Deleting the tag** is allowed, as for any tag; its domains are left with 
no backing tag and no members until a new one is set. There is no domain-side 
audit of this.
+- **Tag object listings**: a backing tag is a tag association of a `DOMAIN` 
object, so `GET /tags/{tag}/objects` and `Tag#associatedObjects` include the 
domains the tag backs, filtered by the domain visibility rule (see Status). A 
tag owner can then see which domains the tag backs.
+
+### Subdomains
+
+Every operation that sets a parent (create with `parent`, attach, move) checks:
+
+| # | Constraint |
+| :---- | :---- |
+| C1 | A domain cannot be its own parent |
+| C2 | The parent is top-level |
+| C3 | The attached domain has no subdomains |
+| C4 | Parent and child are in the same metalake |
+| C5 | At most one parent; attaching a domain that has one is a move |
+| C6 | A `PUBLISHED` domain can only be attached under a `PUBLISHED` parent |
+
+C1–C3 keep the hierarchy one level deep and acyclic, so no cycle detection is 
needed.
+
+- **Attach / move** re-points the child's single edge in one transaction. 
Attaching to the current parent is a no-op.
+- **Detach** removes the edge; the child becomes top-level.
+- **Locking**: each operation locks the domain rows involved (child, new 
parent, old parent) in id order, then checks the constraints. Otherwise two 
valid calls could together create two levels, a cycle, or a dangling edge.
+- **Delete** is refused while the domain has subdomains. Deleting removes the 
domain's own parent edge and its backing tag association. The tag and the tags 
on assets stay.
+- **Membership and scope**: membership comes from backing tags only (see 
Membership and inheritance), so an asset tagged only `Finance/Trading` is a 
member of Trading, not of Finance. A top-level domain's *scope* is its own 
members plus the members of the subdomains the caller can see; a subdomain's 
scope is its own members. Member listing and domain filters use scope, as in 
Unity Catalog, where a subdomain's assets also appear under its parent domain.
+
+### Status
+
+- New domains are `DRAFT`. The owner or a `MANAGE_DOMAIN` holder publishes or 
unpublishes, at any time and without approval.
+- **Visibility is the same on every surface** (REST, MCP, UI). A `DRAFT` 
domain is visible only to its owner and `MANAGE_DOMAIN` holders, including 
those on its parent. For anyone else it does not exist: lists omit it, get 
returns 404, and it is not in `list_domains`, member listings, an object's 
domains, a tag's objects or a parent's roll-up. Responses carry `status`, so a 
caller that sees drafts can filter them out.
+- **Parent names** are the one exception: `parent` is always returned, even 
when the caller cannot see the parent. Only the name is exposed; getting the 
parent still returns 404, and nothing can be done on it. This happens only to 
the owner or child-level managers of a `DRAFT` subdomain under a parent they 
cannot see (C6 keeps published subdomains under published parents), or to a 
caller with privileges on a subdomain but no `USE_DOMAIN` on its published 
parent.
+- **Consistency**: publishing a subdomain needs a published parent; 
unpublishing needs no published subdomains (C6 covers attach and move).
+- Assets can be tagged into a `DRAFT` domain to prepare it; anyone with 
`APPLY_TAG` on its tag can do so, as in Unity Catalog.
+- Roll-ups depend on the caller: someone who can see a `DRAFT` subdomain sees 
its members under the parent; others do not.
+
+### Membership and inheritance
+
+- **Rule**: membership resolves an object's effective tags without filtering 
them by tag privileges. `MetadataObjectTagOperations#listTagsForMetadataObject` 
cannot be reused as is: it drops the tags the caller cannot load 
(`LOAD_TAG_AUTHORIZATION_EXPRESSION`: owner, `VIEW_TAG` or `APPLY_TAG`), so a 
caller without tag privileges would lose the backing tag before matching. Its 
inheritance resolution moves into a shared helper that the tag endpoint keeps 
filtering and domain matching uses unfiltered; domain visibility and object 
access are applied afterwards. An object's effective tags are its own plus its 
schema's and catalog's; for the same tag the nearest level wins, and different 
tags accumulate. A leaf object (see Member types) is a member of domain D when 
its effective tags include D's backing (tag, value).
+- **Dedicated tags (the default)** accumulate: with schema `Finance` and table 
`Sales`, the table is in both, and a child cannot leave a domain it inherits, 
as in Unity Catalog.
+- **Values of one tag** backing different domains override each other: with 
schema `dept=finance` and table `dept=hr`, the table is only in the `dept=hr` 
domain, unless it uses `dept=[finance, hr]`. This is the one case where a child 
can leave an inherited domain, and it follows from the tag owner's choice of 
binding.
+- **Member types**: leaf objects only: tables, views, filesets, topics, models 
and semantic models (`DomainMember.SUPPORTED_TYPES`). Catalogs and schemas are 
never members: one that carries a backing tag passes membership to the leaf 
objects under it through tag inheritance, as in Unity Catalog, where tagging a 
catalog or schema makes the assets inside it appear in the domain. Columns and 
functions are not members either: a column or function carrying a backing tag 
is not listed. A type that becomes taggable later joins only when it is added 
to this list. Metrics belong through their semantic model.
+- **Domains of an object**: the object's effective tags mapped to the domains 
the caller can see. Only the domains the object is a member of are returned; a 
parent reached only through a subdomain is not, and clients show it from 
`parent` (for example `Finance › Trading`). An object that is a member of both 
Finance and Trading gets both. On a catalog or schema, which is never a member, 
it returns the domains whose backing tag the catalog's or schema's own 
effective tags match: this marks it as carrying the domain for the objects 
under it, not as a member. For an inherited membership, `inheritedFrom` names 
the catalog or schema it comes from; it is absent for direct membership. The 
source is known while resolving inheritance, at no extra cost.
+- **Domain filters**, for APIs that filter objects by domain, match scope, 
consistent with member listing: a filter on Finance accepts members of Finance 
and of its visible subdomains; a filter on a subdomain accepts only its own 
members. Each candidate object is checked with the rule above instead of 
listing all members first.
+
+### Member listing
+
+`GET /domains/{domain}/objects` lists leaf objects, expanding catalogs and 
schemas:
+
+1. Resolve the backing tags of the domains in the selected scopes (by default 
the listed domain's whole scope: itself and its visible subdomains).
+2. List the objects carrying them (`listMetadataObjectsForTag`, by tag and 
value). Leaf objects are candidates as they are; a tagged catalog or schema is 
replaced by the leaf objects under it, from Gravitino's store.
+3. Keep the candidates whose effective tags match a selected backing tag (the 
matching rules above, nearest level wins), so a leaf object that overrides an 
inherited value drops out.
+4. Drop objects the caller cannot access (`filterMetadataObject`), 
de-duplicate, and record the subdomain each member comes from (see 
`DomainMemberDTO`). No tag privilege is needed.
+
+- **Store only**: expansion uses the objects recorded in Gravitino's store. 
Objects in an external catalog that Gravitino has not recorded yet are not 
listed.
+- **Cost**: a domain backed by a tag on a large catalog lists every recorded 
leaf object under it. Paging is a follow-up, not part of Phase 1 (see REST API).
+- **Counts**: a domain's asset count is the number of expanded leaf members, 
consistent with the listing.
+
+### Access control
+
+**Domain privileges**
+
+| Privilege | On the metalake | On a domain |
+| :---- | :---- | :---- |
+| `MANAGE_DOMAIN` | Create domains and manage all of them | Edit, publish, 
subdomains, backing tag, delete; see it while `DRAFT` |
+| `USE_DOMAIN` | See all published domains and browse their members | See this 
domain and browse its members |
+
+`MANAGE_DOMAIN` includes `USE_DOMAIN`; owners and the metalake owner hold 
both. Privileges on a parent also apply to its subdomains, as in Unity Catalog. 
There is no separate `CREATE_DOMAIN`.

Review Comment:
   **Parent → subdomain privilege inheritance does not fit today's 
authorization model, and the parent is mutable**
   
   This line, together with L103 (draft visible to `MANAGE_DOMAIN` holders "on 
its parent"), L104, L155 (detach via the parent's privilege) and L165, depends 
on a capability the authorization framework does not have yet. I think it 
deserves its own design paragraph.
   
   **How inheritance works today.** Expressions such as `ANY(OWNER, METALAKE, 
CATALOG, SCHEMA)` are expanded textually into `METALAKE::OWNER || 
CATALOG::OWNER || SCHEMA::OWNER` 
([`AuthorizationExpressionConverter#replaceAnyExpressions`](https://github.com/apache/gravitino/blob/19d88d7bad614f45d50b68aecd32824cbb4bdb30/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java#L142)).
 Each scope is resolved from the request's identifier path 
`metalake.catalog.schema.table`, so an ancestor is always derivable from the 
name.
   
   **Why that does not carry over to domains.**
   - `DOMAIN` is a single-level name 
([`MetadataObjects.parse`](https://github.com/apache/gravitino/blob/19d88d7bad614f45d50b68aecd32824cbb4bdb30/api/src/main/java/org/apache/gravitino/MetadataObjects.java#L179-L189)
 keeps single-level types whole), and L62 makes domains addressable by name 
alone.
   - The parent lives only in `DomainRel`, and it can change.
   - So no `X::MANAGE_DOMAIN` scope can name "the current parent of this 
domain".
   - L165 says the declarative check covers the domain itself because there is 
a single `{domain}` path parameter. That is exactly why it cannot cover the 
inherited part.
   
   **What has to be designed.**
   1. **A new scope resolver** that looks up the parent in `DomainRel` during 
authorization. That lookup runs on every check, including per item when 
`filterMetadataObject` filters a domain list or a parent's roll-up. Listing N 
domains means N parent lookups unless batched.
   2. **Invalidation across nodes.** Whatever the lookup reads (the entity 
cache of `Domain`/`DomainRel`, or anything precomputed in the authorizer) must 
be invalidated on other nodes after move, detach and delete, through the 
changelog. Otherwise node B keeps authorizing the old parent's principals after 
node A has moved the subdomain.
   3. **Move and detach change access without any grant changing.** For example:
      - Alice has `USE_DOMAIN` and Bob has `MANAGE_DOMAIN` on Finance. 
Published Trading is under Finance, so both see it and Bob manages it.
      - Carol, holding `MANAGE_DOMAIN` on Trading and on Markets, moves Trading 
under Markets. No role was touched, yet Alice loses visibility and Bob loses 
management, while Markets' holders gain both.
      - Per L155, Bob alone can also detach Trading using only his inherited 
Finance privilege. After that he has no privilege on Trading and cannot 
re-attach it.
   
      If this is intended, please document that structural operations are 
access-changing, and make sure their events carry enough detail (old and new 
parent) for an auditor reconstructing "why could X see Y".
   
   **Options I see.**
   - **(a)** No inheritance in Phase 1. Grants on a subdomain are explicit; its 
creator owns it, and the metalake owner / metalake-level `MANAGE_DOMAIN` still 
covers everything. It is simpler, matches the "one level, keep it small" 
spirit, and inheritance can be added later as a pure widening.
   - **(b)** Keep inheritance, and specify the `PARENT_DOMAIN`-style scope: how 
it is resolved (one lookup, batched for lists), which cache it reads, which 
changelog entries invalidate it, and the access-changing semantics of move and 
detach above.
   
   Which one do you prefer? I'm fine with either, as long as the doc spells it 
out before the authorization task in Phase 1 starts.
   



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