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());
+      }
+    }
+  }
+}

Reply via email to