This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 7da9b6fe63 [#12729] fix(authz): Authorize generic view operations
(#12797)
7da9b6fe63 is described below
commit 7da9b6fe6328e491899e602716f69cc4a616b49b
Author: mchades <[email protected]>
AuthorDate: Thu Sep 3 13:19:15 2026 +0800
[#12729] fix(authz): Authorize generic view operations (#12797)
### What changes were proposed in this pull request?
- Register generic `ViewOperations` with `GravitinoInterceptionService`.
- Add authorization expressions and metadata for list, create, load,
alter, and drop endpoints.
- Filter listed views by ownership or `SELECT_VIEW`.
- Set the generic View creator as owner and preserve authorization
mappings across rename.
- Reuse `CREATE_VIEW`, `SELECT_VIEW`, and owner-based mutation semantics
without introducing an `ALTER_VIEW` privilege.
- Update authorization, OpenAPI, and View design documentation.
### Why are the changes needed?
Generic View REST operations currently bypass metadata authorization,
allowing requests to reach catalog lookup before privilege checks.
Fix: #12729
### Does this PR introduce _any_ user-facing change?
Yes. When authorization is enabled:
- Generic View endpoints enforce metadata-operation authorization.
- View listings only return authorized entries.
- Newly created Views are owned by their creator.
- Alter and drop operations are owner-based.
This does not add an explicit `INVOKER`/`DEFINER` option, `DEFINER`
execution, or new engine integration. The current Iceberg engine path
continues to use invoker behavior.
### How was this patch tested?
- Ran 50 focused authorization, interceptor, REST, ownership-hook, and
Iceberg View tests.
- Ran Spotless checks for all affected Java modules.
- Ran `./gradlew :docs:build --no-daemon`.
- Ran `git diff origin/main...HEAD --check`.
---
.../java/org/apache/gravitino/GravitinoEnv.java | 10 +-
.../apache/gravitino/hook/ViewHookDispatcher.java | 134 +++++++++++++
.../gravitino/hook/TestViewHookDispatcher.java | 164 +++++++++++++++
design-docs/gravitino-logical-view-management.md | 48 ++---
docs/open-api/views.yaml | 17 +-
docs/security/access-control.md | 26 ++-
docs/tables-and-views.md | 14 +-
.../server/web/filter/RenameViewAuthzHandler.java | 17 +-
.../TestIcebergViewAuthorizationExpression.java | 3 +
.../AuthorizationExpressionConstants.java | 22 +-
.../web/filter/GravitinoInterceptionService.java | 2 +
.../gravitino/server/web/rest/ViewOperations.java | 68 +++++--
.../filter/TestGravitinoInterceptionService.java | 62 ++++++
.../server/web/rest/TestViewOperations.java | 41 ++++
.../TestViewAuthorizationExpression.java | 221 +++++++++++++++++++++
15 files changed, 769 insertions(+), 80 deletions(-)
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index 9de4db1b34..fea34df90a 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -73,6 +73,7 @@ import org.apache.gravitino.hook.SchemaHookDispatcher;
import org.apache.gravitino.hook.TableHookDispatcher;
import org.apache.gravitino.hook.TagHookDispatcher;
import org.apache.gravitino.hook.TopicHookDispatcher;
+import org.apache.gravitino.hook.ViewHookDispatcher;
import org.apache.gravitino.job.BuiltInJobTemplateEventListener;
import org.apache.gravitino.job.JobManager;
import org.apache.gravitino.job.JobOperationDispatcher;
@@ -874,10 +875,8 @@ public class GravitinoEnv {
new FunctionEventDispatcher(eventBus, functionNormalizeDispatcher);
this.functionDispatcher = new
FunctionHookDispatcher(functionEventDispatcher);
- // View operation chain: ViewEventDispatcher -> ViewNormalizeDispatcher ->
- // ViewOperationDispatcher.
- // TODO(#11007): Add ViewHookDispatcher for view ownership and privilege
hooks when view
- // privilege support is finalized.
+ // View operation chain: ViewHookDispatcher -> ViewEventDispatcher ->
ViewNormalizeDispatcher
+ // -> ViewOperationDispatcher.
ViewOperationDispatcher viewOperationDispatcher =
new ViewOperationDispatcher(catalogManager, entityStore, idGenerator,
secretManager);
this.internalViewDispatcher = viewOperationDispatcher;
@@ -894,7 +893,8 @@ public class GravitinoEnv {
new ViewNormalizeDispatcher(internalViewOperationDispatcher,
catalogManager);
ViewEventDispatcher viewEventDispatcher =
new ViewEventDispatcher(eventBus, viewNormalizeDispatcher);
- this.viewDispatcher = viewEventDispatcher;
+ this.viewDispatcher =
+ new ViewHookDispatcher(viewEventDispatcher, this::ownerDispatcher,
catalogManager);
// Semantic Model operation chain: SemanticModelNormalizeDispatcher ->
// SemanticModelOperationDispatcher -> ManagedSemanticModelOperations.
diff --git
a/core/src/main/java/org/apache/gravitino/hook/ViewHookDispatcher.java
b/core/src/main/java/org/apache/gravitino/hook/ViewHookDispatcher.java
new file mode 100644
index 0000000000..2273cd3d3e
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/hook/ViewHookDispatcher.java
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+package org.apache.gravitino.hook;
+
+import java.util.Map;
+import java.util.function.Supplier;
+import javax.annotation.Nullable;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.Owner;
+import org.apache.gravitino.authorization.OwnerDispatcher;
+import org.apache.gravitino.catalog.CapabilityHelpers;
+import org.apache.gravitino.catalog.CatalogManager;
+import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+import org.apache.gravitino.exceptions.ViewAlreadyExistsException;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Representation;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewChange;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.PrincipalUtils;
+
+/**
+ * {@code ViewHookDispatcher} decorates a {@link ViewDispatcher} with
ownership and authorization
+ * lifecycle hooks.
+ */
+public class ViewHookDispatcher implements ViewDispatcher {
+ private final ViewDispatcher dispatcher;
+ private final Supplier<OwnerDispatcher> ownerDispatcher;
+ private final CatalogManager catalogManager;
+
+ /**
+ * Creates a view hook dispatcher.
+ *
+ * @param dispatcher the underlying view dispatcher
+ * @param ownerDispatcher supplies the owner dispatcher, or {@code null}
when authorization is
+ * disabled
+ * @param catalogManager the catalog manager used to apply catalog
capabilities
+ */
+ public ViewHookDispatcher(
+ ViewDispatcher dispatcher,
+ Supplier<OwnerDispatcher> ownerDispatcher,
+ CatalogManager catalogManager) {
+ this.dispatcher = dispatcher;
+ this.ownerDispatcher = ownerDispatcher;
+ this.catalogManager = catalogManager;
+ }
+
+ @Override
+ public NameIdentifier[] listViews(Namespace namespace) throws
NoSuchSchemaException {
+ return dispatcher.listViews(namespace);
+ }
+
+ @Override
+ public View loadView(NameIdentifier ident) throws NoSuchViewException {
+ return dispatcher.loadView(ident);
+ }
+
+ @Override
+ public boolean viewExists(NameIdentifier ident) {
+ return dispatcher.viewExists(ident);
+ }
+
+ @Override
+ public View createView(
+ NameIdentifier ident,
+ @Nullable String comment,
+ Column[] columns,
+ Representation[] representations,
+ @Nullable String defaultCatalog,
+ @Nullable String defaultSchema,
+ Map<String, String> properties)
+ throws NoSuchSchemaException, ViewAlreadyExistsException {
+ View view =
+ dispatcher.createView(
+ ident, comment, columns, representations, defaultCatalog,
defaultSchema, properties);
+
+ OwnerDispatcher ownerManager = ownerDispatcher.get();
+ if (ownerManager != null) {
+ NameIdentifier normalizedIdent =
+ CapabilityHelpers.applyCapabilities(ident, Capability.Scope.VIEW,
catalogManager);
+ ownerManager.setOwner(
+ normalizedIdent.namespace().level(0),
+ NameIdentifierUtil.toMetadataObject(normalizedIdent,
Entity.EntityType.VIEW),
+ PrincipalUtils.getCurrentUserName(),
+ Owner.Type.USER);
+ }
+ return view;
+ }
+
+ @Override
+ public View alterView(NameIdentifier ident, ViewChange... changes)
+ throws NoSuchViewException, IllegalArgumentException {
+ ViewChange.RenameView lastRenameChange = null;
+ for (ViewChange change : changes) {
+ if (change instanceof ViewChange.RenameView) {
+ lastRenameChange = (ViewChange.RenameView) change;
+ }
+ }
+
+ View alteredView = dispatcher.alterView(ident, changes);
+ if (lastRenameChange != null) {
+ AuthorizationUtils.authorizationPluginRenamePrivileges(
+ ident, Entity.EntityType.VIEW, lastRenameChange.getNewName());
+ }
+ return alteredView;
+ }
+
+ @Override
+ public boolean dropView(NameIdentifier ident) {
+ return dispatcher.dropView(ident);
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/hook/TestViewHookDispatcher.java
b/core/src/test/java/org/apache/gravitino/hook/TestViewHookDispatcher.java
new file mode 100644
index 0000000000..099a89478a
--- /dev/null
+++ b/core/src/test/java/org/apache/gravitino/hook/TestViewHookDispatcher.java
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+package org.apache.gravitino.hook;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.Owner;
+import org.apache.gravitino.authorization.OwnerDispatcher;
+import org.apache.gravitino.catalog.CatalogManager;
+import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.connector.capability.CapabilityResult;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Representation;
+import org.apache.gravitino.rel.SQLRepresentation;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewChange;
+import org.apache.gravitino.utils.PrincipalUtils;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+/** Tests for {@link ViewHookDispatcher}. */
+public class TestViewHookDispatcher {
+
+ private static final String METALAKE = "metalake";
+ private static final String CATALOG = "catalog";
+
+ @Test
+ public void testCreateViewSetsOwnerWithNormalizedIdentifier() throws
Exception {
+ CatalogManager catalogManager = Mockito.mock(CatalogManager.class);
+ CatalogManager.CatalogWrapper wrapper =
Mockito.mock(CatalogManager.CatalogWrapper.class);
+ Mockito.when(wrapper.capabilities()).thenReturn(new
CaseInsensitiveCapability());
+ Mockito.when(catalogManager.loadCatalogAndWrap(any())).thenReturn(wrapper);
+
+ OwnerDispatcher ownerDispatcher = Mockito.mock(OwnerDispatcher.class);
+ ViewDispatcher dispatcher = Mockito.mock(ViewDispatcher.class);
+ View createdView = Mockito.mock(View.class);
+ Mockito.when(dispatcher.createView(any(), any(), any(), any(), any(),
any(), any()))
+ .thenReturn(createdView);
+ ViewHookDispatcher hook =
+ new ViewHookDispatcher(dispatcher, () -> ownerDispatcher,
catalogManager);
+ NameIdentifier ident = NameIdentifier.of(METALAKE, CATALOG, "SCHEMA_NORM",
"MY_VIEW");
+
+ try (MockedStatic<PrincipalUtils> principalUtils =
Mockito.mockStatic(PrincipalUtils.class)) {
+
principalUtils.when(PrincipalUtils::getCurrentUserName).thenReturn("creator");
+ assertSame(createdView, createView(hook, ident));
+ }
+
+ ArgumentCaptor<MetadataObject> captor =
ArgumentCaptor.forClass(MetadataObject.class);
+ Mockito.verify(ownerDispatcher)
+ .setOwner(eq(METALAKE), captor.capture(), eq("creator"),
eq(Owner.Type.USER));
+ assertEquals(MetadataObject.Type.VIEW, captor.getValue().type());
+ assertEquals("my_view", captor.getValue().name());
+ assertEquals(CATALOG + ".schema_norm", captor.getValue().parent());
+ }
+
+ @Test
+ public void testCreateViewSkipsOwnerWhenAuthorizationDisabled() {
+ CatalogManager catalogManager = Mockito.mock(CatalogManager.class);
+ ViewDispatcher dispatcher = Mockito.mock(ViewDispatcher.class);
+ View createdView = Mockito.mock(View.class);
+ Mockito.when(dispatcher.createView(any(), any(), any(), any(), any(),
any(), any()))
+ .thenReturn(createdView);
+ ViewHookDispatcher hook = new ViewHookDispatcher(dispatcher, () -> null,
catalogManager);
+
+ assertSame(
+ createdView, createView(hook, NameIdentifier.of(METALAKE, CATALOG,
"schema", "view")));
+
+ Mockito.verifyNoInteractions(catalogManager);
+ }
+
+ @Test
+ public void testCreateViewPropagatesOwnerFailure() throws Exception {
+ OwnerDispatcher ownerDispatcher = Mockito.mock(OwnerDispatcher.class);
+ Mockito.doThrow(new RuntimeException("Set owner failed"))
+ .when(ownerDispatcher)
+ .setOwner(any(), any(), any(), any());
+ CatalogManager catalogManager = Mockito.mock(CatalogManager.class);
+ CatalogManager.CatalogWrapper wrapper =
Mockito.mock(CatalogManager.CatalogWrapper.class);
+ Mockito.when(wrapper.capabilities()).thenReturn(Capability.DEFAULT);
+ Mockito.when(catalogManager.loadCatalogAndWrap(any())).thenReturn(wrapper);
+ ViewDispatcher dispatcher = Mockito.mock(ViewDispatcher.class);
+ Mockito.when(dispatcher.createView(any(), any(), any(), any(), any(),
any(), any()))
+ .thenReturn(Mockito.mock(View.class));
+ ViewHookDispatcher hook =
+ new ViewHookDispatcher(dispatcher, () -> ownerDispatcher,
catalogManager);
+
+ RuntimeException thrown =
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ createView(hook, NameIdentifier.of(METALAKE, CATALOG,
"schema", "owner_failure")));
+
+ assertEquals("Set owner failed", thrown.getMessage());
+ }
+
+ @Test
+ public void testRenameViewUpdatesAuthorizationMapping() {
+ ViewDispatcher dispatcher = Mockito.mock(ViewDispatcher.class);
+ ViewHookDispatcher hook =
+ new ViewHookDispatcher(dispatcher, () -> null,
Mockito.mock(CatalogManager.class));
+ NameIdentifier ident = NameIdentifier.of(METALAKE, CATALOG, "schema",
"view");
+ View alteredView = Mockito.mock(View.class);
+ ViewChange setChange = ViewChange.setProperty("key", "value");
+ ViewChange renameChange = ViewChange.rename("newName");
+ Mockito.when(dispatcher.alterView(ident,
setChange)).thenReturn(alteredView);
+ Mockito.when(dispatcher.alterView(ident,
renameChange)).thenReturn(alteredView);
+
+ try (MockedStatic<AuthorizationUtils> authorizationUtils =
+ Mockito.mockStatic(AuthorizationUtils.class)) {
+ assertSame(alteredView, hook.alterView(ident, setChange));
+ authorizationUtils.verifyNoInteractions();
+
+ assertSame(alteredView, hook.alterView(ident, renameChange));
+ authorizationUtils.verify(
+ () ->
+ AuthorizationUtils.authorizationPluginRenamePrivileges(
+ ident, Entity.EntityType.VIEW, "newName"));
+ }
+ }
+
+ private View createView(ViewHookDispatcher hook, NameIdentifier ident) {
+ Representation[] representations =
+ new Representation[] {
+ SQLRepresentation.builder().withDialect("trino").withSql("SELECT
1").build()
+ };
+ return hook.createView(
+ ident, "comment", new Column[0], representations, null, null,
ImmutableMap.of());
+ }
+
+ private static class CaseInsensitiveCapability implements Capability {
+ @Override
+ public CapabilityResult caseSensitiveOnName(Scope scope) {
+ return CapabilityResult.unsupported("case-insensitive");
+ }
+ }
+}
diff --git a/design-docs/gravitino-logical-view-management.md
b/design-docs/gravitino-logical-view-management.md
index 774c7c057b..60b0565a34 100644
--- a/design-docs/gravitino-logical-view-management.md
+++ b/design-docs/gravitino-logical-view-management.md
@@ -42,7 +42,7 @@ Apache Gravitino, as a unified metadata management system, is
well-positioned to
3. **Capability-Driven Storage Strategy**: Automatically select the optimal
storage strategy based on each catalog's capabilities — no user-facing storage
mode configuration needed. Gravitino transparently handles delegation,
extension, and full management per catalog type.
-4. **Access Control Integration**: Integrate with Gravitino's existing access
control framework to provide metadata-level privileges (CREATE_VIEW,
SELECT_VIEW, DROP_VIEW). Data-level access control remains the responsibility
of the underlying compute engines.
+4. **Access Control Integration**: Integrate with Gravitino's existing access
control framework to provide metadata-operation authorization through
`CREATE_VIEW`, `SELECT_VIEW`, and ownership for mutations. Data-level access
control remains the responsibility of the underlying compute engines.
5. **Audit Support**: View operations should be auditable with complete audit
information.
@@ -94,7 +94,6 @@ View
│ └── sql: string # The view definition SQL
├── defaultCatalog: string # Optional, shared across all
representations
├── defaultSchema: string # Optional, shared across all
representations
-├── securityMode: enum # DEFINER | INVOKER (planned field)
├── properties: map<string, string> # Extensible key-value properties
└── auditInfo: AuditInfo # Creation/modification timestamps
and users
```
@@ -120,9 +119,7 @@ View
- In storage, these fields are versioned together with the rest of the
replaceable view body.
- View SQL may contain cross-catalog references (e.g.,
`catalog_a.schema.table JOIN catalog_b.schema.table`). The SQL is stored as-is;
neither Gravitino, the IRC, nor the HMS validates, rewrites, or transforms view
SQL at any point. The compute engine is responsible for resolving and executing
cross-catalog queries at runtime.
-- **securityMode**: Declares the security execution model of the view. This is
a metadata property stored by Gravitino and **passed through to the compute
engine** — Gravitino does not enforce it. Whether it takes effect depends on
the engine's capability (e.g., MySQL natively supports DEFINER/INVOKER; Iceberg
and Hive do not).
- - `DEFINER`: the engine should execute the view query with the view owner's
privileges.
- - `INVOKER`: the engine should execute the view query with the querying
user's privileges.
+- **Execution security mode**: The current shared View API and REST DTOs do
not contain a `securityMode` field or an explicit `INVOKER`/`DEFINER` option.
The current Iceberg engine path behaves as `INVOKER`, so callers still need
permission on referenced data. Persisting and enforcing an explicit mode,
including `DEFINER` and the required engine integrations, remains future design
work rather than metadata represented by the current model.
### Capability-Driven Storage Strategy
@@ -368,7 +365,7 @@ When a view is created through Gravitino (e.g., Trino user
runs `CREATE VIEW` vi
| `hive` | Hive native | No special properties | Plain HiveQL text |
| `flink` | Flink native (v1.1) | `is_generic=true`, `flink.schema.N.name`,
`flink.schema.N.data-type` | Plain Flink SQL text |
-**Implementation note:** The Gravitino engine connector must pass sufficient
metadata (beyond just SQL text and columns) for the HMS catalog provider to
reconstruct the engine-native format. For example, the Trino connector needs to
pass the `ConnectorViewDefinition` fields (`catalog`, `schema`, `owner`,
`runAsInvoker`) as view properties, which the HMS provider uses to build the
Base64 JSON blob.
+**Current implementation note:** Gravitino's View model does not carry
Trino-only `owner`, `runAsInvoker`, or SQL-path fields. The HMS provider
therefore encodes Gravitino-created Trino views with `owner=null` in the Trino
payload, `runAsInvoker=true`, and an empty SQL path; this engine-native owner
field is separate from Gravitino metadata ownership. Replacing a native Trino
view whose values cannot be represented is rejected instead of silently
changing its execution semantics. Preserv [...]
**v1 limitation:** Each HMS view stores exactly one dialect — the dialect of
the creating engine. Multi-dialect views in HMS are not supported in v1. For
multi-dialect view support, use IRC-backed catalogs.
@@ -601,7 +598,7 @@ POST
/api/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/views
}
```
-> **Planned field:** `securityMode` remains part of the API design, but the
current shared REST DTOs (`ViewCreateRequest`, `ViewDTO`, `ViewUpdateRequest`)
do not expose it yet.
+> **Current boundary:** The shared REST DTOs (`ViewCreateRequest`, `ViewDTO`,
`ViewUpdateRequest`) do not expose a `securityMode` field. The current Iceberg
path uses invoker behavior; explicit `INVOKER`/`DEFINER` metadata and engine
enforcement are future work.
##### Get View
@@ -741,7 +738,7 @@ DELETE
/api/metalakes/{metalake}/catalogs/{catalog}/schemas/{schema}/views/{view
#### Java API
-The current `client-java` surface exposes the implemented subset above, so
alter examples use `replaceView` / property changes today. Planned fine-grained
helpers such as `addRepresentation`, `updateRepresentation`, and
`updateComment` are not exposed yet. The design-level `securityMode` field is
also not part of the current `createView(...)` signature.
+The current `client-java` surface exposes the implemented subset above, so
alter examples use `replaceView` / property changes today. Planned fine-grained
helpers such as `addRepresentation`, `updateRepresentation`, and
`updateComment` are not exposed yet. The current `createView(...)` signature
has no execution-security-mode parameter.
```java
// Get ViewCatalog interface from catalog
@@ -820,7 +817,7 @@ boolean dropped =
viewCatalog.dropView(NameIdentifier.of("analytics_schema", "cu
```python
from gravitino import NameIdentifier, Namespace
-from gravitino.api.view import SQLRepresentation, SecurityMode
+from gravitino.api.view import SQLRepresentation
# Get ViewCatalog interface
view_catalog = catalog.as_view_catalog()
@@ -848,7 +845,6 @@ view = view_catalog.create_view(
default_schema="sales"
),
],
- security_mode=SecurityMode.DEFINER,
properties={"description": "Customer order summary for analytics"}
)
@@ -885,14 +881,14 @@ dropped =
view_catalog.drop_view(NameIdentifier.of("analytics_schema", "customer
### View Privileges
-Gravitino defines the following privileges for view management, integrated
with the existing access control framework:
+Gravitino defines the following privileges for View metadata operations,
integrated with the existing access control framework:
| Privilege | Description |
|-----------|-------------|
| `CREATE_VIEW` | Permission to create views in a schema |
-| `SELECT_VIEW` | Permission to read view metadata and use the view |
-| `ALTER_VIEW` | Permission to modify view definition |
-| `DROP_VIEW` | Permission to delete a view |
+| `SELECT_VIEW` | Permission to read view metadata |
+
+There are no `ALTER_VIEW` or `DROP_VIEW` privileges. A newly created View is
owned by its creator when authorization is enabled, and View mutations use
ownership.
**Permission Requirements:**
@@ -900,20 +896,23 @@ Gravitino defines the following privileges for view
management, integrated with
|-----------|-------------------|
| Create View | `USE_CATALOG` on catalog + `USE_SCHEMA` on schema +
`CREATE_VIEW` on schema |
| Read View | `USE_CATALOG` on catalog + `USE_SCHEMA` on schema +
`SELECT_VIEW` on view |
-| Alter View | `USE_CATALOG` on catalog + `USE_SCHEMA` on schema +
`ALTER_VIEW` on view |
-| Drop View | `USE_CATALOG` on catalog + `USE_SCHEMA` on schema + `DROP_VIEW`
on view |
+| List Views | Access to the schema; results are filtered by View ownership or
`SELECT_VIEW` |
+| Alter View | `USE_CATALOG` on catalog + `USE_SCHEMA` on schema + ownership
of the view |
+| Drop View | `USE_CATALOG` on catalog + `USE_SCHEMA` on schema + ownership of
the view |
-> **Note:** These privileges control access to view **metadata** in Gravitino.
Access to the **underlying data** when executing view queries is controlled by
the compute engine and underlying catalog's permission system.
+> **Scope:** These checks authorize View **metadata operations** through the
native Gravitino REST API and the Iceberg REST Catalog. They do not grant
access to referenced tables or authorize query execution.
-> **Relationship with `securityMode`**: The `securityMode` field
(DEFINER/INVOKER) is a metadata property stored by Gravitino but enforced by
the compute engine at query execution time. Whether it takes effect depends on
engine capability — for example, MySQL natively supports DEFINER/INVOKER
semantics, while Iceberg and Hive do not. For engines without native support,
the field serves as a metadata annotation only. In summary: View Privileges
govern *who can manage view metadata through [...]
+> **Execution boundary:** The current Iceberg engine path behaves as
`INVOKER`, and the caller must have access to the underlying data. The current
View model has no explicit `INVOKER`/`DEFINER` option. This authorization
delivery does not implement `DEFINER` behavior or add an engine integration.
---
### Engine Adaptation
-#### Trino Integration
+The examples in this section describe possible future engine adapters. They
are not implemented by the metadata-operation authorization described above,
and they must not be read as support for an explicit execution security mode in
the current View API.
+
+#### Proposed Trino Integration
-Gravitino's Trino connector implements the view-related interfaces in
[`ConnectorMetadata`](https://github.com/trinodb/trino/blob/480/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java#L957):
+A future Gravitino Trino connector could implement the view-related interfaces
in
[`ConnectorMetadata`](https://github.com/trinodb/trino/blob/480/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java#L957):
```java
public class GravitinoConnectorMetadata implements ConnectorMetadata {
@@ -933,10 +932,7 @@ public class GravitinoConnectorMetadata implements
ConnectorMetadata {
.withSql(definition.getOriginalSql())
.withDefaultCatalog(definition.getCatalog().orElse(null))
.withDefaultSchema(definition.getSchema().orElse(null))
- .build())
- .withSecurityMode(definition.isRunAsInvoker()
- ? SecurityMode.INVOKER
- : SecurityMode.DEFINER);
+ .build());
// Add columns
for (ViewColumn col : definition.getColumns()) {
@@ -965,8 +961,8 @@ public class GravitinoConnectorMetadata implements
ConnectorMetadata {
Optional.ofNullable(rep.getDefaultSchema()),
convertColumns(view.getColumns()),
Optional.ofNullable(view.getComment()),
- Optional.empty(), // owner managed via Gravitino's owner_meta
- view.getSecurityMode() == SecurityMode.INVOKER
+ Optional.empty(), // owner managed via Gravitino's owner metadata
+ true // current View model can only be represented safely as
INVOKER
));
}
diff --git a/docs/open-api/views.yaml b/docs/open-api/views.yaml
index 483d3532de..2fd0bb0519 100644
--- a/docs/open-api/views.yaml
+++ b/docs/open-api/views.yaml
@@ -30,6 +30,9 @@ paths:
- view
summary: List views
operationId: listViews
+ description: >
+ Lists views after authorizing access to the schema. When authorization
is enabled, the
+ returned identifiers are filtered to views the caller owns or can read
with SELECT_VIEW.
responses:
"200":
$ref: "./openapi.yaml#/components/responses/EntityListResponse"
@@ -56,6 +59,9 @@ paths:
- view
summary: Create view
operationId: createView
+ description: >
+ Creates a view. When authorization is enabled, the caller needs
USE_CATALOG, USE_SCHEMA,
+ and CREATE_VIEW in scope and becomes the owner of the created view.
requestBody:
content:
application/json:
@@ -106,7 +112,9 @@ paths:
- view
summary: Get view
operationId: loadView
- description: Return the specified view object
+ description: >
+ Returns the specified view metadata. When authorization is enabled,
the caller needs
+ USE_CATALOG, USE_SCHEMA, and SELECT_VIEW in scope, or ownership.
responses:
"200":
$ref: "#/components/responses/ViewResponse"
@@ -133,7 +141,9 @@ paths:
- view
summary: Update view
operationId: alterView
- description: Update the specified view in a schema
+ description: >
+ Updates the specified view metadata in a schema. When authorization is
enabled, this is an
+ owner-only mutation; there is no ALTER_VIEW privilege.
requestBody:
content:
application/json:
@@ -176,6 +186,9 @@ paths:
- view
summary: Delete view
operationId: dropView
+ description: >
+ Deletes the specified view metadata. When authorization is enabled,
this is an owner-only
+ mutation; there is no DROP_VIEW privilege.
responses:
"200":
$ref: "./openapi.yaml#/components/responses/DropResponse"
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index 7d6b29e7b5..8bcbccb424 100755
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -176,7 +176,7 @@ sets the scope of the grant. Binding a privilege to a type
not listed for it is
| `SELECT_TABLE` | Metalake, Catalog, Schema, Table | Read any
table in scope |
| `MODIFY_TABLE` | Metalake, Catalog, Schema, Table | Read and
write to, and alter the schema of, any table in scope |
| `CREATE_VIEW` | Metalake, Catalog, Schema | Create views
in any schema in scope |
-| `SELECT_VIEW` | Metalake, Catalog, Schema, View | Read any
view in scope |
+| `SELECT_VIEW` | Metalake, Catalog, Schema, View | Read view
metadata in scope |
| `CREATE_TOPIC` | Metalake, Catalog, Schema | Create
topics in any schema in scope |
| `CONSUME_TOPIC` | Metalake, Catalog, Schema, Topic | Consume from
any topic in scope |
| `PRODUCE_TOPIC` | Metalake, Catalog, Schema, Topic | Consume
from, produce to, and alter any topic in scope |
@@ -190,8 +190,9 @@ sets the scope of the grant. Binding a privilege to a type
not listed for it is
| `EXECUTE_FUNCTION` | Metalake, Catalog, Schema, Function | Read the
metadata of, and execute, any function in scope |
| `MODIFY_FUNCTION` | Metalake, Catalog, Schema, Function | Alter or
drop any function in scope |
-Either `SELECT_TABLE` or `MODIFY_TABLE` is enough to load a table's metadata,
and the same pairing
-holds for views, topics, and filesets.
+Either `SELECT_TABLE` or `MODIFY_TABLE` is enough to load a table's metadata.
Topics and filesets
+have similar read/write privilege pairs. Views do not have a modify privilege:
`SELECT_VIEW` reads
+view metadata, while altering or dropping a view is owner-only.
`CREATE_MODEL` and `CREATE_MODEL_VERSION` are deprecated aliases for
`REGISTER_MODEL` and
`LINK_MODEL_VERSION`. They resolve to identical authorization, so existing
grants keep working, but
@@ -235,8 +236,8 @@ Three rules apply throughout, so they are not repeated
below:
- Reaching an object inside a catalog and a schema also requires `USE_CATALOG`
and `USE_SCHEMA`.
- A privilege counts whether it is held on the object itself or on any
ancestor.
-List operations never fail. They return the entries the caller is entitled to
see, which for a
-metalake owner is all of them.
+List operations first require access to their parent scope. After that gateway
check succeeds, they
+return only the entries the caller is entitled to see, which for a metalake
owner is all of them.
#### Data Objects
@@ -255,8 +256,19 @@ Table statistics follow the table itself: reading them
takes `SELECT_TABLE` or `
writing them takes `MODIFY_TABLE`. Model versions follow the model:
`USE_MODEL` to read, owner to
alter or delete. Fetching a credential takes whatever loading the object takes.
-Renaming a table or view into a different schema is the one operation needing
a privilege on a second
-object: the owner of the table or view, plus `CREATE_TABLE` or `CREATE_VIEW`
on the target schema.
+The View row applies to metadata operations through both the native Gravitino
REST API and the
+Iceberg REST Catalog when authorization is enabled. Listing first requires
access to the schema and
+then filters individual views by ownership or `SELECT_VIEW`. Creating a view
makes the caller its
+owner, which is the path used for later alter and drop operations.
+
+These checks authorize View metadata operations only. They do not grant access
to referenced tables
+or authorize SQL execution. The current Iceberg engine path uses invoker
semantics, so the caller
+still needs access to the underlying data. The View API has no explicit
`INVOKER`/`DEFINER` option,
+and Gravitino does not implement `DEFINER` execution or a new engine
integration as part of this
+authorization behavior.
+
+The native View rename operation changes only the name within the existing
schema and remains
+owner-only; it does not accept a target schema.
#### Metalake Objects
diff --git a/docs/tables-and-views.md b/docs/tables-and-views.md
index e1adb7c923..caf0c89e0d 100644
--- a/docs/tables-and-views.md
+++ b/docs/tables-and-views.md
@@ -187,14 +187,24 @@ a specific field rather than a whole table. Policies
attach at the table level.
| Privilege | Grantable on | What it allows
|
|----------------|-------------------------------------|---------------------------------|
-| `CREATE_TABLE` | Metalake, catalog, or schema | Creating tables and
views |
-| `SELECT_TABLE` | Metalake, catalog, schema, or table | Reading a table or
view |
+| `CREATE_TABLE` | Metalake, catalog, or schema | Creating tables
|
+| `SELECT_TABLE` | Metalake, catalog, schema, or table | Reading table
metadata |
| `MODIFY_TABLE` | Metalake, catalog, schema, or table | Writing to and
altering a table |
+| `CREATE_VIEW` | Metalake, catalog, or schema | Creating views
|
+| `SELECT_VIEW` | Metalake, catalog, schema, or view | Reading view metadata
|
Granting at a wider scope covers everything beneath it. Dropping a table is
reserved for the
metalake owner and the object owner, and ownership resolves down the
hierarchy, so the owner of a
catalog has the owner path to every table in it.
+When metadata authorization is enabled, listing views first requires access to
the schema and then
+returns only views the caller owns or can read with `SELECT_VIEW`. Creating a
view requires
+`CREATE_VIEW` in scope and makes the caller its owner; altering and dropping a
view are owner-only.
+
+View permissions cover metadata operations only. The API does not yet define
an `INVOKER` or
+`DEFINER` execution mode, so access to referenced data remains subject to the
engine's
+authorization.
+
## Using the API
Tables and views can be created, listed, altered, and dropped over REST and
through the Java and
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/RenameViewAuthzHandler.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/RenameViewAuthzHandler.java
index 08630981bd..865498e989 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/RenameViewAuthzHandler.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/RenameViewAuthzHandler.java
@@ -27,6 +27,7 @@ import org.apache.gravitino.Entity.EntityType;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.authorization.AuthorizationRequestContext;
import
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
import
org.apache.gravitino.server.web.filter.BaseMetadataAuthorizationMethodInterceptor.AuthorizationHandler;
import org.apache.gravitino.utils.HierarchicalSchemaUtil;
@@ -107,9 +108,9 @@ public class RenameViewAuthzHandler implements
AuthorizationHandler {
}
/**
- * Validates authorization for cross-namespace view renames following MySQL
privilege model: -
- * Requires ownership on source view (equivalent to DROP privilege) -
Requires CREATE_VIEW
- * privilege on destination schema
+ * Validates authorization for cross-namespace view renames: ownership is
required on the source
+ * view, and {@code CREATE_VIEW} is required on the destination schema.
Gravitino does not define
+ * a {@code DROP_VIEW} privilege.
*
* @param catalog The catalog name
* @param metalakeName The metalake name
@@ -134,10 +135,7 @@ public class RenameViewAuthzHandler implements
AuthorizationHandler {
EntityType.VIEW,
NameIdentifierUtil.ofView(metalakeName, catalog, sourceSchema,
sourceView));
- String sourceExpression =
- "ANY(OWNER, METALAKE, CATALOG) || "
- + "SCHEMA_OWNER_WITH_USE_CATALOG || "
- + "ANY_USE_CATALOG && ANY_USE_SCHEMA && VIEW::OWNER";
+ String sourceExpression =
AuthorizationExpressionConstants.VIEW_OWNER_AUTHORIZATION_EXPRESSION;
AuthorizationExpressionEvaluator sourceEvaluator =
new AuthorizationExpressionEvaluator(sourceExpression);
@@ -162,10 +160,7 @@ public class RenameViewAuthzHandler implements
AuthorizationHandler {
destContext.put(
EntityType.SCHEMA, NameIdentifierUtil.ofSchema(metalakeName, catalog,
destSchema));
- String destExpression =
- "ANY(OWNER, METALAKE, CATALOG) || "
- + "SCHEMA_OWNER_WITH_USE_CATALOG || "
- + "ANY_USE_CATALOG && ANY_USE_SCHEMA && ANY_CREATE_VIEW";
+ String destExpression =
AuthorizationExpressionConstants.CREATE_VIEW_AUTHORIZATION_EXPRESSION;
AuthorizationExpressionEvaluator destEvaluator =
new AuthorizationExpressionEvaluator(destExpression);
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
index 44bd419f4e..dc843ff3fb 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
@@ -447,6 +447,9 @@ public class TestIcebergViewAuthorizationExpression {
assertTrue(
mockEvaluator.getResult(
ImmutableSet.of("SCHEMA::SELECT_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ assertFalse(
+ mockEvaluator.getResult(
+ ImmutableSet.of("SCHEMA::CREATE_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
// DENY_SELECT_VIEW blocks
assertFalse(
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
index 60374880b8..7aadba3f2c 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
@@ -139,15 +139,11 @@ public class AuthorizationExpressionConstants {
"""
ANY(OWNER, METALAKE, CATALOG) ||
SCHEMA_OWNER_WITH_USE_CATALOG ||
- ANY_USE_CATALOG && ANY_USE_SCHEMA && (VIEW::OWNER ||
ANY_SELECT_VIEW || ANY_CREATE_VIEW)
+ ANY_USE_CATALOG && ANY_USE_SCHEMA && (VIEW::OWNER ||
ANY_SELECT_VIEW)
""";
public static final String ICEBERG_LOAD_VIEW_AUTHORIZATION_EXPRESSION =
- """
- ANY(OWNER, METALAKE, CATALOG) ||
- SCHEMA_OWNER_WITH_USE_CATALOG ||
- ANY_USE_CATALOG && ANY_USE_SCHEMA && (VIEW::OWNER ||
ANY_SELECT_VIEW)
- """;
+ LOAD_VIEW_AUTHORIZATION_EXPRESSION;
/**
* Existence-check expression for Iceberg REST {@code loadView}: when the
primary load-view
@@ -161,21 +157,29 @@ public class AuthorizationExpressionConstants {
(ANY_CREATE_VIEW || TABLE::OWNER || ANY_SELECT_TABLE ||
ANY_MODIFY_TABLE || ANY_CREATE_TABLE)
""";
- public static final String ICEBERG_CREATE_VIEW_AUTHORIZATION_EXPRESSION =
+ /** Creates a view through the generic or Iceberg REST metadata API. */
+ public static final String CREATE_VIEW_AUTHORIZATION_EXPRESSION =
"""
ANY(OWNER, METALAKE, CATALOG) ||
SCHEMA_OWNER_WITH_USE_CATALOG ||
ANY_USE_CATALOG && ANY_USE_SCHEMA && ANY_CREATE_VIEW
""";
- /** Iceberg REST replace view, drop view, and rename view (VIEW::OWNER
path). */
- public static final String ICEBERG_VIEW_OWNER_AUTHORIZATION_EXPRESSION =
+ public static final String ICEBERG_CREATE_VIEW_AUTHORIZATION_EXPRESSION =
+ CREATE_VIEW_AUTHORIZATION_EXPRESSION;
+
+ /** View alter, drop, and rename operations use the {@code VIEW::OWNER}
path. */
+ public static final String VIEW_OWNER_AUTHORIZATION_EXPRESSION =
"""
ANY(OWNER, METALAKE, CATALOG) ||
SCHEMA_OWNER_WITH_USE_CATALOG ||
ANY_USE_CATALOG && ANY_USE_SCHEMA && VIEW::OWNER
""";
+ /** Iceberg REST replace view, drop view, and rename view (VIEW::OWNER
path). */
+ public static final String ICEBERG_VIEW_OWNER_AUTHORIZATION_EXPRESSION =
+ VIEW_OWNER_AUTHORIZATION_EXPRESSION;
+
/** Iceberg REST {@code HEAD .../views/{view}} (view exists). */
public static final String ICEBERG_VIEW_EXISTS_AUTHORIZATION_EXPRESSION =
"""
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index 8c5e24dac3..8b3f16c01b 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -77,6 +77,7 @@ import org.apache.gravitino.server.web.rest.TableOperations;
import org.apache.gravitino.server.web.rest.TagOperations;
import org.apache.gravitino.server.web.rest.TopicOperations;
import org.apache.gravitino.server.web.rest.UserOperations;
+import org.apache.gravitino.server.web.rest.ViewOperations;
import org.apache.gravitino.utils.PrincipalUtils;
import org.glassfish.hk2.api.Descriptor;
import org.glassfish.hk2.api.Filter;
@@ -99,6 +100,7 @@ public class GravitinoInterceptionService implements
InterceptionService {
CatalogOperations.class.getName(),
SchemaOperations.class.getName(),
TableOperations.class.getName(),
+ ViewOperations.class.getName(),
ModelOperations.class.getName(),
FunctionOperations.class.getName(),
TopicOperations.class.getName(),
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
index 77e5fbc938..794298b25f 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
@@ -31,6 +31,8 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.catalog.ViewDispatcher;
@@ -44,6 +46,10 @@ import org.apache.gravitino.dto.util.DTOConverters;
import org.apache.gravitino.metrics.MetricNames;
import org.apache.gravitino.rel.View;
import org.apache.gravitino.rel.ViewChange;
+import org.apache.gravitino.server.authorization.MetadataAuthzHelper;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
import org.apache.gravitino.server.web.Utils;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
@@ -68,10 +74,14 @@ public class ViewOperations {
@Produces("application/vnd.gravitino.v1+json")
@Timed(name = "list-view." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
@ResponseMetered(name = "list-view", absolute = true)
+ @AuthorizationExpression(
+ expression =
AuthorizationExpressionConstants.LOAD_SCHEMA_AUTHORIZATION_EXPRESSION,
+ accessMetadataType = MetadataObject.Type.SCHEMA)
public Response listViews(
- @PathParam("metalake") String metalake,
- @PathParam("catalog") String catalog,
- @PathParam("schema") String schema) {
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("catalog") @AuthorizationMetadata(type =
Entity.EntityType.CATALOG) String catalog,
+ @PathParam("schema") @AuthorizationMetadata(type =
Entity.EntityType.SCHEMA) String schema) {
LOG.info("Received list views request for schema: {}.{}.{}", metalake,
catalog, schema);
try {
return Utils.doAs(
@@ -79,6 +89,12 @@ public class ViewOperations {
() -> {
Namespace viewNS = NamespaceUtil.ofView(metalake, catalog, schema);
NameIdentifier[] idents = dispatcher.listViews(viewNS);
+ idents =
+ MetadataAuthzHelper.filterByExpression(
+ metalake,
+
AuthorizationExpressionConstants.FILTER_VIEW_AUTHORIZATION_EXPRESSION,
+ Entity.EntityType.VIEW,
+ idents);
Response response = Utils.ok(new EntityListResponse(idents));
LOG.info(
"List {} views under schema: {}.{}.{}", idents.length,
metalake, catalog, schema);
@@ -94,10 +110,14 @@ public class ViewOperations {
@Produces("application/vnd.gravitino.v1+json")
@Timed(name = "create-view." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
@ResponseMetered(name = "create-view", absolute = true)
+ @AuthorizationExpression(
+ expression =
AuthorizationExpressionConstants.CREATE_VIEW_AUTHORIZATION_EXPRESSION,
+ accessMetadataType = MetadataObject.Type.SCHEMA)
public Response createView(
- @PathParam("metalake") String metalake,
- @PathParam("catalog") String catalog,
- @PathParam("schema") String schema,
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("catalog") @AuthorizationMetadata(type =
Entity.EntityType.CATALOG) String catalog,
+ @PathParam("schema") @AuthorizationMetadata(type =
Entity.EntityType.SCHEMA) String schema,
ViewCreateRequest request) {
if (request == null) {
LOG.warn("Received create view request with null request body");
@@ -141,11 +161,15 @@ public class ViewOperations {
@Produces("application/vnd.gravitino.v1+json")
@Timed(name = "load-view." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
@ResponseMetered(name = "load-view", absolute = true)
+ @AuthorizationExpression(
+ expression =
AuthorizationExpressionConstants.LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+ accessMetadataType = MetadataObject.Type.VIEW)
public Response loadView(
- @PathParam("metalake") String metalake,
- @PathParam("catalog") String catalog,
- @PathParam("schema") String schema,
- @PathParam("view") String view) {
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("catalog") @AuthorizationMetadata(type =
Entity.EntityType.CATALOG) String catalog,
+ @PathParam("schema") @AuthorizationMetadata(type =
Entity.EntityType.SCHEMA) String schema,
+ @PathParam("view") @AuthorizationMetadata(type = Entity.EntityType.VIEW)
String view) {
LOG.info("Received load view request for view: {}.{}.{}.{}", metalake,
catalog, schema, view);
try {
return Utils.doAs(
@@ -167,11 +191,15 @@ public class ViewOperations {
@Produces("application/vnd.gravitino.v1+json")
@Timed(name = "alter-view." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
@ResponseMetered(name = "alter-view", absolute = true)
+ @AuthorizationExpression(
+ expression =
AuthorizationExpressionConstants.VIEW_OWNER_AUTHORIZATION_EXPRESSION,
+ accessMetadataType = MetadataObject.Type.VIEW)
public Response alterView(
- @PathParam("metalake") String metalake,
- @PathParam("catalog") String catalog,
- @PathParam("schema") String schema,
- @PathParam("view") String view,
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("catalog") @AuthorizationMetadata(type =
Entity.EntityType.CATALOG) String catalog,
+ @PathParam("schema") @AuthorizationMetadata(type =
Entity.EntityType.SCHEMA) String schema,
+ @PathParam("view") @AuthorizationMetadata(type = Entity.EntityType.VIEW)
String view,
ViewUpdatesRequest request) {
LOG.info("Received alter view request: {}.{}.{}.{}", metalake, catalog,
schema, view);
if (request == null) {
@@ -208,11 +236,15 @@ public class ViewOperations {
@Produces("application/vnd.gravitino.v1+json")
@Timed(name = "drop-view." + MetricNames.HTTP_PROCESS_DURATION, absolute =
true)
@ResponseMetered(name = "drop-view", absolute = true)
+ @AuthorizationExpression(
+ expression =
AuthorizationExpressionConstants.VIEW_OWNER_AUTHORIZATION_EXPRESSION,
+ accessMetadataType = MetadataObject.Type.VIEW)
public Response dropView(
- @PathParam("metalake") String metalake,
- @PathParam("catalog") String catalog,
- @PathParam("schema") String schema,
- @PathParam("view") String view) {
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("catalog") @AuthorizationMetadata(type =
Entity.EntityType.CATALOG) String catalog,
+ @PathParam("schema") @AuthorizationMetadata(type =
Entity.EntityType.SCHEMA) String schema,
+ @PathParam("view") @AuthorizationMetadata(type = Entity.EntityType.VIEW)
String view) {
LOG.info("Received drop view request: {}.{}.{}.{}", metalake, catalog,
schema, view);
try {
return Utils.doAs(
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
index 615e0810d3..95165b0576 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
@@ -51,6 +51,7 @@ import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.Privilege;
import org.apache.gravitino.catalog.TableDispatcher;
+import org.apache.gravitino.catalog.ViewDispatcher;
import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.exceptions.ForbiddenException;
@@ -67,9 +68,11 @@ import
org.apache.gravitino.server.authorization.annotations.AuthorizationObject
import
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
import org.apache.gravitino.server.web.Utils;
import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
+import org.apache.gravitino.server.web.rest.ViewOperations;
import org.apache.gravitino.tag.TagDispatcher;
import org.apache.gravitino.utils.PrincipalUtils;
import org.apache.gravitino.utils.RequestContext;
+import org.glassfish.hk2.api.Descriptor;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -86,6 +89,65 @@ public class TestGravitinoInterceptionService {
RequestContext.clear();
}
+ @Test
+ public void testViewOperationsIsRegisteredForInterception() {
+ Descriptor descriptor = mock(Descriptor.class);
+
when(descriptor.getImplementation()).thenReturn(ViewOperations.class.getName());
+
+ Assertions.assertTrue(
+ new
GravitinoInterceptionService().getDescriptorFilter().matches(descriptor));
+ }
+
+ @Test
+ public void testDeniedListViewsDoesNotReachDispatcher() throws Throwable {
+ try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
+ MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+ mockStatic(GravitinoAuthorizerProvider.class);
+ MockedStatic<AuthorizationUtils> authUtilsMocked =
mockStatic(AuthorizationUtils.class);
+ MockedStatic<GravitinoEnv> envMocked = mockStatic(GravitinoEnv.class))
{
+ principalUtilsMocked
+ .when(PrincipalUtils::getCurrentPrincipal)
+ .thenReturn(new UserPrincipal("tester"));
+
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+ authUtilsMocked
+ .when(
+ () ->
+ AuthorizationUtils.checkCurrentUser(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation -> null);
+
+ GravitinoAuthorizerProvider provider =
mock(GravitinoAuthorizerProvider.class);
+ GravitinoAuthorizer authorizer = mock(GravitinoAuthorizer.class);
+
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+ when(provider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+ GravitinoEnv env = mock(GravitinoEnv.class);
+ EventBus eventBus = mock(EventBus.class);
+ envMocked.when(GravitinoEnv::getInstance).thenReturn(env);
+ when(env.eventBus()).thenReturn(eventBus);
+
+ ViewDispatcher dispatcher = mock(ViewDispatcher.class);
+ ViewOperations operations = new ViewOperations(dispatcher);
+ FieldUtils.writeField(operations, "httpRequest",
mock(HttpServletRequest.class), true);
+ Method method =
+ ViewOperations.class.getMethod("listViews", String.class,
String.class, String.class);
+ MethodInvocation invocation = mock(MethodInvocation.class);
+ when(invocation.getMethod()).thenReturn(method);
+ when(invocation.getArguments())
+ .thenReturn(new Object[] {"testMetalake", "testCatalog",
"testSchema"});
+ when(invocation.proceed())
+ .thenAnswer(ignored -> operations.listViews("testMetalake",
"testCatalog", "testSchema"));
+
+ MethodInterceptor interceptor =
+ new
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+ Response response = (Response) interceptor.invoke(invocation);
+
+ assertEquals(Response.Status.FORBIDDEN.getStatusCode(),
response.getStatus());
+ verify(invocation, never()).proceed();
+ verify(dispatcher, never()).listViews(any());
+ }
+ }
+
@Test
public void testMetadataAuthorizationMethodInterceptor() throws Throwable {
try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestViewOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestViewOperations.java
index c82253c497..215ed2be8b 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestViewOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestViewOperations.java
@@ -23,9 +23,13 @@ import static
org.apache.gravitino.Configs.ENABLE_AUTHORIZATION;
import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.apache.gravitino.Entity.EntityType.VIEW;
+import static
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.FILTER_VIEW_AUTHORIZATION_EXPRESSION;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
@@ -65,15 +69,18 @@ import org.apache.gravitino.rel.SQLRepresentation;
import org.apache.gravitino.rel.View;
import org.apache.gravitino.rel.ViewChange;
import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.server.authorization.MetadataAuthzHelper;
import org.apache.gravitino.server.web.mapper.JsonMappingExceptionMapper;
import org.apache.gravitino.server.web.mapper.JsonParseExceptionMapper;
import org.apache.gravitino.server.web.mapper.JsonProcessingExceptionMapper;
+import org.apache.gravitino.utils.NamespaceUtil;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.TestProperties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
public class TestViewOperations extends BaseOperationsTest {
@@ -187,6 +194,40 @@ public class TestViewOperations extends BaseOperationsTest
{
Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResp2.getType());
}
+ @Test
+ public void testListViewsFiltersUnauthorizedEntries() throws
IllegalAccessException {
+ NameIdentifier visibleView = NameIdentifier.of(metalake, catalog, schema,
"visible");
+ NameIdentifier hiddenView = NameIdentifier.of(metalake, catalog, schema,
"hidden");
+ NameIdentifier[] listedViews = new NameIdentifier[] {visibleView,
hiddenView};
+ NameIdentifier[] filteredViews = new NameIdentifier[] {visibleView};
+ ViewDispatcher localDispatcher = mock(ViewDispatcher.class);
+ when(localDispatcher.listViews(NamespaceUtil.ofView(metalake, catalog,
schema)))
+ .thenReturn(listedViews);
+
+ ViewOperations operations = new ViewOperations(localDispatcher);
+ FieldUtils.writeField(operations, "httpRequest",
mock(HttpServletRequest.class), true);
+ try (MockedStatic<MetadataAuthzHelper> metadataAuthzHelper =
+ mockStatic(MetadataAuthzHelper.class)) {
+ metadataAuthzHelper
+ .when(
+ () ->
+ MetadataAuthzHelper.filterByExpression(
+ metalake, FILTER_VIEW_AUTHORIZATION_EXPRESSION, VIEW,
listedViews))
+ .thenReturn(filteredViews);
+
+ Response response = operations.listViews(metalake, catalog, schema);
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
response.getStatus());
+ EntityListResponse listResponse = (EntityListResponse)
response.getEntity();
+ Assertions.assertArrayEquals(filteredViews, listResponse.identifiers());
+ verify(localDispatcher).listViews(NamespaceUtil.ofView(metalake,
catalog, schema));
+ metadataAuthzHelper.verify(
+ () ->
+ MetadataAuthzHelper.filterByExpression(
+ metalake, FILTER_VIEW_AUTHORIZATION_EXPRESSION, VIEW,
listedViews));
+ }
+ }
+
@Test
public void testLoadView() {
View view = mockView("view1", "comment", ImmutableMap.of("k", "v"),
"trino", "SELECT 1");
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestViewAuthorizationExpression.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestViewAuthorizationExpression.java
new file mode 100644
index 0000000000..9643cb6a9a
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestViewAuthorizationExpression.java
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+package org.apache.gravitino.server.web.rest.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.ImmutableSet;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import ognl.OgnlException;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.dto.requests.ViewCreateRequest;
+import org.apache.gravitino.dto.requests.ViewUpdatesRequest;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import org.apache.gravitino.server.web.rest.ViewOperations;
+import org.junit.jupiter.api.Test;
+
+/** Tests authorization expressions and metadata parameters on generic view
REST endpoints. */
+public class TestViewAuthorizationExpression {
+
+ @Test
+ public void testListViewAuthorization() throws NoSuchMethodException,
OgnlException {
+ Method method =
+ ViewOperations.class.getMethod("listViews", String.class,
String.class, String.class);
+ assertAuthorizationAnnotation(
+ method,
+ AuthorizationExpressionConstants.LOAD_SCHEMA_AUTHORIZATION_EXPRESSION,
+ MetadataObject.Type.SCHEMA);
+ assertMetadataTypes(
+ method, Entity.EntityType.METALAKE, Entity.EntityType.CATALOG,
Entity.EntityType.SCHEMA);
+
+ MockAuthorizationExpressionEvaluator gateway = evaluator(method);
+ assertFalse(gateway.getResult(ImmutableSet.of()));
+ assertTrue(gateway.getResult(ImmutableSet.of("METALAKE::OWNER")));
+ assertTrue(gateway.getResult(ImmutableSet.of("CATALOG::OWNER")));
+ assertFalse(gateway.getResult(ImmutableSet.of("SCHEMA::USE_SCHEMA")));
+ assertTrue(gateway.getResult(ImmutableSet.of("CATALOG::USE_CATALOG",
"SCHEMA::USE_SCHEMA")));
+
+ MockAuthorizationExpressionEvaluator filter =
+ new MockAuthorizationExpressionEvaluator(
+
AuthorizationExpressionConstants.FILTER_VIEW_AUTHORIZATION_EXPRESSION);
+ assertFalse(filter.getResult(ImmutableSet.of()));
+ assertTrue(filter.getResult(ImmutableSet.of("VIEW::OWNER")));
+ assertTrue(filter.getResult(ImmutableSet.of("SCHEMA::SELECT_VIEW")));
+ assertFalse(filter.getResult(ImmutableSet.of("SCHEMA::CREATE_VIEW")));
+ assertFalse(
+ filter.getResult(ImmutableSet.of("METALAKE::SELECT_VIEW",
"CATALOG::DENY_SELECT_VIEW")));
+ }
+
+ @Test
+ public void testCreateViewAuthorization() throws NoSuchMethodException,
OgnlException {
+ Method method =
+ ViewOperations.class.getMethod(
+ "createView", String.class, String.class, String.class,
ViewCreateRequest.class);
+ assertAuthorizationAnnotation(
+ method,
+ AuthorizationExpressionConstants.CREATE_VIEW_AUTHORIZATION_EXPRESSION,
+ MetadataObject.Type.SCHEMA);
+ assertMetadataTypes(
+ method,
+ Entity.EntityType.METALAKE,
+ Entity.EntityType.CATALOG,
+ Entity.EntityType.SCHEMA,
+ null);
+
+ MockAuthorizationExpressionEvaluator evaluator = evaluator(method);
+ assertFalse(evaluator.getResult(ImmutableSet.of()));
+ assertTrue(evaluator.getResult(ImmutableSet.of("METALAKE::OWNER")));
+ assertTrue(evaluator.getResult(ImmutableSet.of("CATALOG::OWNER")));
+ assertFalse(evaluator.getResult(ImmutableSet.of("SCHEMA::OWNER")));
+ assertTrue(evaluator.getResult(ImmutableSet.of("SCHEMA::OWNER",
"CATALOG::USE_CATALOG")));
+ assertFalse(evaluator.getResult(ImmutableSet.of("SCHEMA::CREATE_VIEW",
"SCHEMA::USE_SCHEMA")));
+ assertTrue(
+ evaluator.getResult(
+ ImmutableSet.of("SCHEMA::CREATE_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ assertFalse(
+ evaluator.getResult(
+ ImmutableSet.of("SCHEMA::SELECT_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ }
+
+ @Test
+ public void testLoadViewAuthorization() throws NoSuchMethodException,
OgnlException {
+ Method method =
+ ViewOperations.class.getMethod(
+ "loadView", String.class, String.class, String.class,
String.class);
+ assertAuthorizationAnnotation(
+ method,
+ AuthorizationExpressionConstants.LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+ MetadataObject.Type.VIEW);
+ assertViewMetadataTypes(method);
+
+ MockAuthorizationExpressionEvaluator evaluator = evaluator(method);
+ assertFalse(evaluator.getResult(ImmutableSet.of()));
+ assertTrue(evaluator.getResult(ImmutableSet.of("METALAKE::OWNER")));
+ assertFalse(evaluator.getResult(ImmutableSet.of("VIEW::OWNER")));
+ assertTrue(
+ evaluator.getResult(
+ ImmutableSet.of("VIEW::OWNER", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ assertTrue(
+ evaluator.getResult(
+ ImmutableSet.of("SCHEMA::SELECT_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ assertFalse(
+ evaluator.getResult(
+ ImmutableSet.of("SCHEMA::CREATE_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ assertFalse(
+ evaluator.getResult(
+ ImmutableSet.of(
+ "METALAKE::SELECT_VIEW",
+ "CATALOG::DENY_SELECT_VIEW",
+ "METALAKE::USE_SCHEMA",
+ "METALAKE::USE_CATALOG")));
+ }
+
+ @Test
+ public void testAlterViewAuthorization() throws NoSuchMethodException,
OgnlException {
+ Method method =
+ ViewOperations.class.getMethod(
+ "alterView",
+ String.class,
+ String.class,
+ String.class,
+ String.class,
+ ViewUpdatesRequest.class);
+ assertOwnerMutationAuthorization(method);
+ assertMetadataTypes(
+ method,
+ Entity.EntityType.METALAKE,
+ Entity.EntityType.CATALOG,
+ Entity.EntityType.SCHEMA,
+ Entity.EntityType.VIEW,
+ null);
+ }
+
+ @Test
+ public void testDropViewAuthorization() throws NoSuchMethodException,
OgnlException {
+ Method method =
+ ViewOperations.class.getMethod(
+ "dropView", String.class, String.class, String.class,
String.class);
+ assertOwnerMutationAuthorization(method);
+ assertViewMetadataTypes(method);
+ }
+
+ private void assertOwnerMutationAuthorization(Method method) throws
OgnlException {
+ assertAuthorizationAnnotation(
+ method,
+ AuthorizationExpressionConstants.VIEW_OWNER_AUTHORIZATION_EXPRESSION,
+ MetadataObject.Type.VIEW);
+ MockAuthorizationExpressionEvaluator evaluator = evaluator(method);
+ assertFalse(evaluator.getResult(ImmutableSet.of()));
+ assertTrue(evaluator.getResult(ImmutableSet.of("METALAKE::OWNER")));
+ assertFalse(evaluator.getResult(ImmutableSet.of("VIEW::OWNER")));
+ assertTrue(
+ evaluator.getResult(
+ ImmutableSet.of("VIEW::OWNER", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ assertFalse(
+ evaluator.getResult(
+ ImmutableSet.of("SCHEMA::SELECT_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ assertFalse(
+ evaluator.getResult(
+ ImmutableSet.of("SCHEMA::CREATE_VIEW", "SCHEMA::USE_SCHEMA",
"CATALOG::USE_CATALOG")));
+ }
+
+ private MockAuthorizationExpressionEvaluator evaluator(Method method) {
+ return new MockAuthorizationExpressionEvaluator(
+ method.getAnnotation(AuthorizationExpression.class).expression());
+ }
+
+ private void assertAuthorizationAnnotation(
+ Method method, String expression, MetadataObject.Type metadataType) {
+ AuthorizationExpression annotation =
method.getAnnotation(AuthorizationExpression.class);
+ assertNotNull(annotation);
+ assertEquals(expression, annotation.expression());
+ assertEquals(metadataType, annotation.accessMetadataType());
+ }
+
+ private void assertViewMetadataTypes(Method method) {
+ assertMetadataTypes(
+ method,
+ Entity.EntityType.METALAKE,
+ Entity.EntityType.CATALOG,
+ Entity.EntityType.SCHEMA,
+ Entity.EntityType.VIEW);
+ }
+
+ private void assertMetadataTypes(Method method, Entity.EntityType...
expectedTypes) {
+ Parameter[] parameters = method.getParameters();
+ assertEquals(expectedTypes.length, parameters.length);
+ for (int i = 0; i < parameters.length; i++) {
+ AuthorizationMetadata annotation =
parameters[i].getAnnotation(AuthorizationMetadata.class);
+ if (expectedTypes[i] == null) {
+ assertNull(annotation);
+ } else {
+ assertNotNull(annotation);
+ assertEquals(expectedTypes[i], annotation.type());
+ }
+ }
+ }
+}