Skip to content

[#12562] feat(lance): Add table mutation and deletion authorization - #12696

Merged
yuqi1129 merged 3 commits into
apache:mainfrom
yuqi1129:feat/12562-lance-table-mutation-authz
Sep 3, 2026
Merged

[#12562] feat(lance): Add table mutation and deletion authorization#12696
yuqi1129 merged 3 commits into
apache:mainfrom
yuqi1129:feat/12562-lance-table-mutation-authz

Conversation

@yuqi1129

@yuqi1129 yuqi1129 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Authorizes the four Lance REST table endpoints that were still served unchecked after #12559, #12560 and #12561: deregister, drop, drop_columns and alter_columns.

  • drop_columns and alter_columns require MODIFY_TABLE or ownership.
  • deregister and drop require ownership of the table or of one of its ancestors. MODIFY_TABLE alters a table but never removes it.
  • Both expressions are guarded on the addressed entity type, so an identifier that resolves to a catalog or a schema is denied instead of being authorized as if it addressed a table.

deregister (which removes only the Gravitino metadata) and drop (which also deletes the data) deliberately require the same privilege. Neither the Gravitino core server nor Iceberg REST splits removal into two levels, and inventing a third rule here would be a difference callers have to learn for no benefit.

This is the last endpoint group of #9087, so with this PR the Lance REST surface is fully authorized. The remaining sub-task is the documentation one, #12574.

Fix: #12562

Why are the changes needed?

With authorization enabled, any authenticated caller could drop any table or rewrite any table's columns through Lance REST, regardless of the privileges they hold on it.

Does this PR introduce any user-facing change?

Yes. With authorization enabled, column changes, deregister and drop are authorized and denied requests return 403 before any metadata or storage mutation. A table the caller may not remove is reported as forbidden whether or not it exists, so these endpoints cannot be used to probe for existence.

How was this patch tested?

TestLanceMetadataAuthorizationMethodInterceptor and LanceTableAuthorizationIT cover the two properties this change is about: a caller without the privilege is rejected before the operation runs, and a table the caller cannot see is indistinguishable from one that does not exist.

  • MODIFY_TABLE authorizes column changes and SELECT_TABLE does not; an explicit deny overrides MODIFY_TABLE; MODIFY_TABLE and CREATE_TABLE together are still not enough to remove a table, while ownership is.
  • The ancestor-ownership branches are covered per level rather than by making the caller own everything: owning the schema removes tables the owner never created, owning a catalog or the metalake does too, and schema ownership without USE_CATALOG removes nothing.
  • Denied paths assert verify(invocation, never()).proceed(), which is the direct form of "no mutation on denial". In the integration test the allowed column changes are checked against the described columns ([id, value] -> [id, renamed] -> [id]), so they are shown to reach the metadata store rather than merely passing the interceptor.
  • A denied response is checked not to contain the table location, and an inaccessible table and a missing table both return 403 to the unprivileged caller while the admin sees 200 and 404.

New TestLanceRESTEndpointAuthorizationCoverage guards the completeness of the whole surface. The shared pipeline authorizes a method only when it carries @AuthorizationExpression, so adding an endpoint without one serves it unchecked and no behavioral test would notice, because the new endpoint simply has no test of its own. The test scans the REST resource package instead of listing endpoints, and asserts that every JAX-RS endpoint declares an expression and that every resource declaring one is actually intercepted. Health operations are the one exception and are listed explicitly. I verified the test fails as intended by removing the two new annotations:

Lance REST endpoints without @AuthorizationExpression are served unchecked:
[LanceTableOperations#deregisterTable, LanceTableOperations#dropTable]

./gradlew :lance:lance-rest-server:build :lance:lance-common:build — 202 tests, all passing.

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Code Coverage Report

Overall Project 68.98% +0.27% 🟢
Files changed 94.61% 🟢

Module Coverage
aliyun 19.74% 🔴
api 51.62% 🟢
authorization-common 85.96% 🟢
authorization-ranger 4.38% 🔴
aws 53.54% 🟢
azure 32.1% 🔴
catalog-common 20.87% 🔴
catalog-fileset 82.17% 🟢
catalog-glue 69.24% 🟢
catalog-hive 82.96% 🟢
catalog-jdbc-common 45.69% 🟢
catalog-jdbc-doris 82.69% 🟢
catalog-jdbc-mysql 79.33% 🟢
catalog-jdbc-postgresql 83.39% 🟢
catalog-jdbc-starrocks 79.16% 🟢
catalog-kafka 76.99% 🟢
catalog-lakehouse-generic 60.55% 🟢
catalog-lakehouse-hudi 79.1% 🟢
catalog-lakehouse-iceberg 85.93% 🟢
catalog-lakehouse-paimon 84.29% 🟢
catalog-model 77.99% 🟢
cli 44.51% 🟢
client-java 77.37% 🟢
common 56.34% 🟢
core 83.83% 🟢
filesystem-hadoop3 76.48% 🟢
flink 0.0% 🔴
flink-common 52.1% 🟢
flink-runtime 0.0% 🔴
gcp 32.2% 🔴
hadoop-auth 68.0% 🟢
hadoop-common 17.84% 🔴
hive-metastore-common 53.4% 🟢
iceberg-aliyun-bundle 0.0% 🔴
iceberg-common 64.75% 🟢
iceberg-rest-server 75.89% 🟢
idp-basic 86.42% 🟢
integration-test-common 0.0% 🔴
jobs 62.92% 🟢
lance-common 32.52% 🔴
lance-rest-server 68.15% +8.25% 🟢
lineage 53.02% 🟢
optimizer 83.24% 🟢
optimizer-api 21.95% 🔴
server 89.35% +1.68% 🟢
server-common 80.67% 🟢
spark 28.57% 🔴
spark-common 52.04% 🟢
tencent 81.78% 🟢
trino-connector 51.26% 🟢
Files
Module File Coverage
lance-rest-server LanceTableOperations.java 97.37% 🟢
LanceAuthorizationExpressions.java 0.0% 🔴
server CatalogOperations.java 100.0% 🟢
OwnerOperations.java 100.0% 🟢
PermissionOperations.java 100.0% 🟢
BulkOperations.java 98.06% 🟢
PolicyOperations.java 97.46% 🟢
MetadataObjectTagOperations.java 93.07% 🟢
MetadataObjectPolicyOperations.java 91.46% 🟢
StatisticOperations.java 90.42% 🟢
MetalakeOperations.java 79.52% 🟢

@yuqi1129
yuqi1129 force-pushed the feat/12562-lance-table-mutation-authz branch from 7a69a61 to f92433a Compare September 2, 2026 15:08
…tion

Column changes require MODIFY_TABLE or ownership, while deregister-table
and drop-table require ownership: MODIFY_TABLE alters a table but never
removes it, matching the Gravitino and Iceberg REST surfaces.

A table the caller may not remove is reported as forbidden whether or
not it exists, so the endpoints cannot be used to probe for existence.

Claude-Session: https://claude.ai/code/session_01EPJduJLZAREBg1bKxybe1e
@yuqi1129
yuqi1129 force-pushed the feat/12562-lance-table-mutation-authz branch from f92433a to d582b7a Compare September 3, 2026 07:52
… ownership

Add a test that scans the REST resource package and fails when an
endpoint carries no authorization expression, or when a resource that
declares one is not intercepted. The shared pipeline authorizes a method
only when it is annotated, so a new endpoint would otherwise be served
unchecked and no behavioral test would notice.

Cover the ancestor-ownership branches of the removal and mutation
expressions directly: owning the schema removes tables the owner never
created, owning a catalog or the metalake does too, and schema
ownership without USE_CATALOG removes nothing.

Claude-Session: https://claude.ai/code/session_01EPJduJLZAREBg1bKxybe1e
@yuqi1129
yuqi1129 marked this pull request as ready for review September 3, 2026 08:03
Copilot AI lite review requested due to automatic review settings September 3, 2026 08:03
@yuqi1129 yuqi1129 self-assigned this Sep 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

One new integration test case asserts a privilege distinction but currently denies due to missing schema/table access rather than demonstrating that SELECT_TABLE doesn’t authorize mutations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Completes authorization coverage for the remaining Lance REST table-mutation and deletion endpoints by adding explicit authorization expressions and extending unit/integration tests to verify denial-before-mutation and concealment semantics.

Changes:

  • Added @AuthorizationExpression guards for deregister, drop, drop_columns, and alter_columns Lance REST endpoints.
  • Introduced DROP_TABLE_AUTHORIZATION_EXPRESSION and applied it to table removal operations; applied modify-table expression to column mutations.
  • Added test coverage, including a new “authorization coverage” test that scans REST resources and asserts all endpoints are annotated and intercepted.
File summaries
File Description
lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/authorization/TestLanceRESTEndpointAuthorizationCoverage.java Adds a package-scan based guard to ensure all REST endpoints are annotated and intercepted.
lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/authorization/TestLanceMetadataAuthorizationMethodInterceptor.java Extends interceptor unit tests for column mutation and table removal authorization semantics (including ancestor ownership).
lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java Extends integration tests to cover mutation/removal authz, concealment, and “no mutation on denial”.
lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceTableOperations.java Adds @AuthorizationExpression annotations to the remaining unguarded table endpoints.
lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/authorization/LanceAuthorizationExpressions.java Introduces DROP_TABLE_AUTHORIZATION_EXPRESSION for table removal operations.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +270 to +273
// Selecting a table does not authorize changing its columns.
assertStatus(403, dropColumns(READER, MUTABLE_TABLE, "value"));
assertStatus(403, alterColumns(READER, MUTABLE_TABLE, "value", "renamed"));
Assertions.assertEquals(List.of("id", "value"), describedColumns(MUTABLE_TABLE));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this was a real problem with the test. READER had no USE_SCHEMA on WRITE_SCHEMA and no SELECT_TABLE on MUTABLE_TABLE, so the 403 only showed that it could not reach the table, not that SELECT_TABLE fails to authorize a column change.

Fixed in 6fd5489: READER now holds USE_SCHEMA on WRITE_SCHEMA and SELECT_TABLE on MUTABLE_TABLE, and the test asserts describe returns 200 for it before the two denials. The only privilege it is missing now is MODIFY_TABLE, so the denials say what the test claims.

… own point

The reader had no access to the write schema at all, so its denied
column changes were evidence that it could not reach the table, not
evidence that SELECT_TABLE fails to authorize a column change.

Grant the reader USE_SCHEMA on the write schema and SELECT_TABLE on the
table it is denied on, and assert it can describe that table before the
denials, so the test states the property it claims to test.

Claude-Session: https://claude.ai/code/session_01EPJduJLZAREBg1bKxybe1e
@yuqi1129
yuqi1129 merged commit 5bf0d8b into apache:main Sep 3, 2026
37 checks passed
@yuqi1129
yuqi1129 deleted the feat/12562-lance-table-mutation-authz branch September 3, 2026 13:54
jerryshao pushed a commit that referenced this pull request Sep 4, 2026
…sions (#12876)

### What changes were proposed in this pull request?

Adds `DROP_TABLE_AUTHORIZATION_EXPRESSION` to
`AuthorizationExpressionConstants` and replaces 13 hand-written copies
of three table authorization expressions with references to the shared
constants:

| Sites | Constant |
| --- | --- |
| `TableOperations#dropTable`, `IcebergTableOperations#dropTable`,
`RenameTableAuthzHandler` | `DROP_TABLE_AUTHORIZATION_EXPRESSION` (new)
|
| `IcebergTableOperations#updateTable`, `IcebergTableRenameOperations`,
four `StatisticOperations` endpoints |
`MODIFY_TABLE_AUTHORIZATION_EXPRESSION` (existing) |
| `IcebergTableOperations` credentials and plan, two
`StatisticOperations` endpoints | `LOAD_TABLE_AUTHORIZATION_EXPRESSION`
(existing) |

Also removes a local variable in `RenameTableAuthzHandler` that only
aliased the expression.

This is behavior preserving. No privilege rule changes.

Lance REST has a fourth copy of the removal expression, added in #12696,
which is not merged yet. The two PRs are independent; whichever lands
second, the Lance copy will be pointed at the same shared constant in a
follow-up so all four surfaces read from one place.

Fix: #12875

### Why are the changes needed?

A privilege change to one of these rules currently has to be repeated in
every copy, and missing one is a silent authorization difference between
the Gravitino, Iceberg REST and Lance REST surfaces rather than a
visible failure. Two copies had already drifted in formatting
(`ANY_SELECT_TABLE|| ANY_MODIFY_TABLE`, missing the space), which is
evidence that they are edited independently.

### Does this PR introduce _any_ user-facing change?

No.

### How was this patch tested?

The expressions were compared before and after rather than by eye. A
throwaway test ran each removed literal and its replacement constant
through `AuthorizationExpressionConverter.convertToOgnlExpression`,
asserted the results are identical ignoring whitespace, and parsed both
with `Ognl.parseExpression` to confirm they are still valid OGNL.
Whitespace around operators turned out to be the only difference, from
the two `StatisticOperations` copies that were missing a space before
`||`.

`./gradlew :server-common:test :server:test
:iceberg:iceberg-rest-server:test` — 1119 tests, all passing.

`TestHttpsServerAuthentication` is a pre-existing flake and is
unrelated: a different method of that class fails the same way on a
branch that does not touch `server-common` at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Subtask] Add table mutation and deletion authorization for Lance REST

3 participants