Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,69 @@ Notes:
(the third argument binds the issuer to that key, so UC only uses it to verify tokens from that issuer).


## Authorization model

Two kinds of privilege are in play, and they are easy to confuse.

| Privilege | What it is | Where it acts |
|---|---|---|
| `USE CATALOG`, `USE SCHEMA` | The right to enter a container. On its own it grants nothing on what is inside; the server requires it on every ancestor of whatever is being reached | Exactly the securable it is granted on |
| `SELECT`, `MODIFY` | The right to read (write) data | The securable it is granted on **and everything beneath it** |

`SELECT` and `MODIFY` inherit down the hierarchy: granted on a schema they cover every table in it,
**including tables created later**; granted on a catalog they cover every schema's tables. The
check on a table resolves through its parents, so nothing is copied and a revoke at the schema or
catalog takes every table with it at once. The reach is the subtree and nothing else: a read on
one schema says nothing about a sibling schema or another catalog. `OWNER` never inherits.
`USE SCHEMA` is checked on each schema, but granted on a *catalog* it inherits to every schema in
it the same way -- which is what a catalog-wide read needs (below).

To read one table a principal therefore needs `USE CATALOG` + `USE SCHEMA` + `SELECT` on the
table. To read every table in a schema, present and future -- what a service account usually
wants -- grant the same two `USE_*` and `SELECT` on the schema instead:

```bash
PATCH /api/2.1/unity-catalog/permissions/catalog/<cat>
{"changes":[{"principal":"<svc>","add":["USE CATALOG"]}]}
PATCH /api/2.1/unity-catalog/permissions/schema/<cat>.<sch>
{"changes":[{"principal":"<svc>","add":["USE SCHEMA","SELECT"]}]}
```

To read every table in every schema of a catalog, schemas created later included, put all three on
the catalog. Without `USE SCHEMA` there the principal cannot enter any schema and the `SELECT`
reaches nothing:

```bash
PATCH /api/2.1/unity-catalog/permissions/catalog/<cat>
{"changes":[{"principal":"<svc>","add":["USE CATALOG","USE SCHEMA","SELECT"]}]}
```

**Who may grant what.** There is no grant option: holding a privilege never includes passing it
on, and a user who only *holds* `SELECT` on a schema can neither grant it to anyone else nor give
it up. The right to grant comes from ownership alone. An owner (whoever created the securable, or
was made its owner) grants on it and on what it contains: `USE_*`, `CREATE_*`, and
`SELECT`/`MODIFY` on a table. Because `SELECT`/`MODIFY` on a **schema or catalog** reach
everything beneath them, only a **metastore owner** may grant or revoke them there -- the
built-in `admin`, and anyone appointed through `PUT /api/1.0/unity-control/metastore/admins/<email>`
(the "super user"). A schema's owner who tries gets `PERMISSION_DENIED`, and a request that mixes
`USE SCHEMA` with `SELECT` is refused as a whole, nothing half-applied. The UI's *Grant access*
dialog offers `read` on a schema or catalog only to a metastore admin for the same reason.

**Who sees the user directory.** Only a metastore owner may create, deactivate, delete or
promote accounts (SCIM `POST`/`PUT`/`PATCH`/`DELETE` on `/scim2/Users`), so the UI shows the
*Users* page to metastore admins only; anyone else gets no menu entry and is sent back to the
catalogs if they open `/users` directly. The SCIM *listing* stays readable by any signed-in user:
the *Grant access* dialog on a catalog, schema or table page picks the grantee from it, and an
owner granting read on their own table needs that list.

`USE CATALOG` and `USE SCHEMA` are deliberately **not** reserved to the metastore owner, on
schemas and catalogs alike. They are entry rights only: with nothing but `USE_*` a user can list
the names of the containers they may enter and reads no table, no column and no data. Reserving
them would break the one thing owners must be able to do on their own -- give a colleague read on a
single table, which needs `USE CATALOG` + `USE SCHEMA` on the ancestors -- and would buy nothing,
because the data right is in `SELECT`/`MODIFY`, and those are reserved. The same reasoning covers
`USE SCHEMA` granted on a catalog: it opens every schema's door, and still no table.

## UI sign-in

The UI requires sign-in. Each way in has an address of its own; the application's address is not one
Expand Down
55 changes: 55 additions & 0 deletions deploy/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,61 @@ ConfigMap 只是"改这个文件"的一种投递方式。
| 换 tag 升级后跑的仍是旧版本 | 整个 `UC_HOME` 被挂成了卷,遮蔽了镜像里的二进制;只挂 `etc/conf` 和 `etc/db` |


## 授权模型

有两类权限,很容易混。

| 权限 | 是什么 | 作用在哪 |
|---|---|---|
| `USE CATALOG`、`USE SCHEMA` | 进入容器的"通行权",本身不授予对里面任何对象的操作;服务端要求对被访问对象的每一层祖先都持有 | 只在被授予的那个对象上 |
| `SELECT`、`MODIFY` | 读(写)数据的权限 | 被授予的对象**及其之下的一切** |

`SELECT` 与 `MODIFY` 沿层级向下继承:授在 schema 上即覆盖其中全部表,**含之后新建的表**;授在
catalog 上即覆盖所有 schema 的表。对表的判定会顺着父子关系向上找,没有任何复制动作,所以在 schema
或 catalog 上撤销,其下所有表同时失效。覆盖范围就是那棵子树,再无其它:对某个 schema 的 read 不涉及
兄弟 schema,也不涉及别的 catalog。`OWNER` 永不继承。`USE SCHEMA` 按 schema 逐个检查,但授在
*catalog* 上时同样会继承到其中每个 schema——catalog 级整体可读正需要这一点(见下)。

因此读一张表需要 `USE CATALOG` + `USE SCHEMA` + 表上的 `SELECT`;要读某个 schema 下的全部表(含
未来的,服务账号通常要的就是这个),把 `SELECT` 授在 schema 上即可:

```bash
PATCH /api/2.1/unity-catalog/permissions/catalog/<cat>
{"changes":[{"principal":"<svc>","add":["USE CATALOG"]}]}
PATCH /api/2.1/unity-catalog/permissions/schema/<cat>.<sch>
{"changes":[{"principal":"<svc>","add":["USE SCHEMA","SELECT"]}]}
```

要读某个 catalog 下所有 schema 的全部表(含之后新建的 schema),把三项都授在 catalog 上。少了
`USE SCHEMA`,该账号进不了任何 schema,`SELECT` 就落不到任何表上:

```bash
PATCH /api/2.1/unity-catalog/permissions/catalog/<cat>
{"changes":[{"principal":"<svc>","add":["USE CATALOG","USE SCHEMA","SELECT"]}]}
```

**谁能授什么。** 没有 grant option:持有某个权限不等于可以转授,只是*持有* schema 上 `SELECT` 的
用户既不能把它授给别人,也不能自己撤掉。能授,只来自 owner 身份。owner(创建者,或被设为 owner 的
人)可以在自己的对象及其内容上授 `USE_*`、`CREATE_*`,以及表上的 `SELECT`/`MODIFY`。因为
**schema 或 catalog** 上的 `SELECT`/`MODIFY` 会覆盖其下一切,这两层上的授予与撤销只有
**metastore owner** 能做——内置的 `admin`,以及经
`PUT /api/1.0/unity-control/metastore/admins/<email>` 任命的账号("超级账号")。schema 的 owner
去做会得到 `PERMISSION_DENIED`;把 `USE SCHEMA` 和 `SELECT` 放在同一个请求里也会整单拒绝,不会
应用一半。UI 的 *Grant access* 对话框在 schema / catalog 上只对 metastore admin 开放 `read` 档,
原因相同。

**谁能看到用户目录。** 建用户、停用、删除、任命管理员(SCIM `/scim2/Users` 的 `POST`/`PUT`/
`PATCH`/`DELETE`)只有 metastore owner 能做,所以 UI 的 *Users* 页只对 metastore admin 显示;其他人
没有菜单入口,直接打开 `/users` 会被送回 catalog 列表。SCIM 的*列表*接口仍对任何登录用户开放:
catalog / schema / table 页的 *Grant access* 对话框要从里面选被授权人,owner 给自己表授 read 需要这份
名单。

`USE CATALOG` 与 `USE SCHEMA` 在 schema 和 catalog 上**有意不收**到 metastore owner 手里。它们只是
通行权:一个用户只有 `USE_*` 时,能列出自己可以进入的容器名字,读不到任何表、任何列、任何数据。
收掉它们会打断 owner 必须能自助完成的一件事——给同事授某一张表的 read,这需要祖先上的
`USE CATALOG` + `USE SCHEMA`——却换不来任何安全收益,因为数据权限在 `SELECT`/`MODIFY` 上,而那两个
已经收了。授在 catalog 上的 `USE SCHEMA` 同理:它打开每个 schema 的门,但仍然没有一张表。

## UI 登录

UI 必须登录。**每个入口都有自己的地址,应用地址不是入口**:没有会话时应用地址会跳到 `<ui>/login`。
Expand Down
7 changes: 7 additions & 0 deletions features.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,10 @@ Feature history of this fork, by PR merge date.

- features: —
- bugfix: `deploy-uc.sh` accepts a path in `UC_EXTERNAL_URL` (`https://api.example.com/unitycatalog`), which the #21 state-cookie fix depends on and which the script rejected outright, so the fix could not be reached through the repo's own deployment path. Trailing slash, empty segment, query, fragment and a missing scheme are still rejected. The sample file and both deployment guides now describe the behind-a-prefix case and the redirect URI to register.

## 2026-10-10

#25 (https://github.com/relytcloud/unitycatalog/pull/25)

- features: Read access at the schema and catalog level. `SELECT` (and `MODIFY`) granted on a schema covers every table beneath it, tables created later included, and granted on a catalog -- together with `USE SCHEMA` on the catalog, which inherits the same way -- covers every schema's tables, schemas created later included; the reach is that subtree and nothing else. Because the reach is wide, granting or revoking `SELECT`/`MODIFY` on a schema or catalog is reserved to metastore administrators (the built-in `admin` and anyone appointed through `PUT /api/1.0/unity-control/metastore/admins/<email>`); owners keep `USE_*`, `CREATE_*` and table-level grants, and a request mixing `USE_*` with `SELECT` is refused as a whole. The UI offers `read` on a schema or catalog page to administrators, lists `read` and `create` rows separately, applies the same gate to the Users page's *Grant access* dialog, and shows the Users page to administrators only. Both deployment guides gain an *Authorization model* section: the two kinds of privilege, inheritance and its limits, the schema-wide and catalog-wide recipes, who may grant what and why `USE_*` is not reserved, and who sees the user directory.
- bugfix: —
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.server.annotation.ExceptionHandler;
import com.linecorp.armeria.server.annotation.Get;
Expand Down Expand Up @@ -283,10 +284,49 @@ public HttpResponse updateCredentialAuthorization(
return updateAuthorization(CREDENTIAL, name, request);
}

/**
* SELECT and MODIFY inherit down the hierarchy: granted on a catalog or schema they cover every
* table beneath it, including tables that do not exist yet (the authorizer resolves a table's
* privileges through its parents, see jcasbin_auth_model.conf's g2). That reach is the reason to
* grant them there -- and the reason only a metastore owner may: a schema's owner, which is
* whoever created it, would otherwise be able to open everything under it. USE_* and CREATE_*
* stay with the owners, because a table's owner granting read on that one table has to be able
* to add the USE_* its ancestors require.
*/
private static final Set<Privilege> HIERARCHY_WIDE_PRIVILEGES =
Set.of(Privilege.SELECT, Privilege.MODIFY);

private void requireMetastoreOwnerForHierarchyWideGrants(
SecurableType securableType, List<PermissionsChange> changes) {
if (securableType != SecurableType.CATALOG && securableType != SecurableType.SCHEMA) {
return;
}
boolean touchesHierarchyWide =
changes.stream()
.anyMatch(
change ->
Stream.concat(change.getAdd().stream(), change.getRemove().stream())
.anyMatch(HIERARCHY_WIDE_PRIVILEGES::contains));
if (!touchesHierarchyWide) {
return;
}
UUID principalId = userRepository.findPrincipalId();
if (!authorizer.authorize(
principalId, metastoreRepository.getMetastoreId(), Privileges.OWNER)) {
throw new BaseException(
ErrorCode.PERMISSION_DENIED,
"SELECT and MODIFY on a "
+ securableType.getValue()
+ " apply to every table beneath it;"
+ " only a metastore owner may grant or revoke them");
}
}

private HttpResponse updateAuthorization(
SecurableType securableType, String name, UpdatePermissions request) {
UUID resourceId = getResourceId(securableType, name);
List<PermissionsChange> changes = request.getChanges();
requireMetastoreOwnerForHierarchyWideGrants(securableType, changes);
Set<UUID> principalIds = new HashSet<>();
changes.forEach(
change -> {
Expand Down
Loading
Loading