This is an automated email from the ASF dual-hosted git repository.

mchades 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 4969517071 [#9535] feat(server): enforce function privileges on 
FunctionOperations (#10882)
4969517071 is described below

commit 49695170715cb9508d9b6b8cec8166773e792dab
Author: mchades <[email protected]>
AuthorDate: Wed Apr 29 11:28:13 2026 +0800

    [#9535] feat(server): enforce function privileges on FunctionOperations 
(#10882)
    
    ### What changes were proposed in this pull request?
    
    Implement server-side authorization enforcement for function operations
    and add end-to-end integration tests.
    
    **Server authorization (`server-common`, `server`)**:
    - `AuthorizationExpressionConstants` — add
    `LOAD_FUNCTION_AUTHORIZATION_EXPRESSION` and
    `FILTER_FUNCTION_AUTHORIZATION_EXPRESSION`
    - `AuthorizationExpressionConverter` — wire `FUNCTION` into the
    `CAN_ACCESS_METADATA` template, and add `ANY_REGISTER_FUNCTION`,
    `ANY_EXECUTE_FUNCTION`, `ANY_MODIFY_FUNCTION` expansions
    - `FunctionOperations` — annotate all 5 endpoints (`registerFunction`,
    `listFunctions`, `getFunction`, `alterFunction`, `dropFunction`) with
    `@AuthorizationExpression`/`@AuthorizationMetadata`; filter
    `listFunctions` results via `MetadataAuthzHelper#filterByExpression`
    
    **Integration test (`client-java`)**:
    - `FunctionAuthorizationIT` — covers register / list / get / alter /
    drop flows using `Catalog.Type.MODEL` schemas (no Docker container
    needed), mirroring `ModelAuthorizationIT`; verifies `REGISTER_FUNCTION`,
    `EXECUTE_FUNCTION`, `MODIFY_FUNCTION` enforcement and list-visibility
    filtering
    
    ### Why are the changes needed?
    
    Without these changes, function endpoints are accessible to any
    authenticated user regardless of their granted privileges.
    
    Fix: #9535
    
    ### Does this PR introduce _any_ user-facing change?
    
    Function REST endpoints now enforce privilege checks:
    - `POST /functions` requires `REGISTER_FUNCTION` (or ownership)
    - `GET /functions/{function}` requires `EXECUTE_FUNCTION` or
    `MODIFY_FUNCTION` (or ownership)
    - `PUT /functions/{function}` requires `MODIFY_FUNCTION` (or ownership)
    - `DELETE /functions/{function}` requires `MODIFY_FUNCTION` (or
    ownership)
    - `GET /functions` returns only functions visible to the caller
    
    ### How was this patch tested?
    
    Integration test `FunctionAuthorizationIT` (5 ordered tests) passes
    locally:
    ```
    ./gradlew :clients:client-java:test --tests "*FunctionAuthorizationIT*" 
-PskipTests -PskipDockerTests=false
    ```
    
    Co-authored-by: Copilot <[email protected]>
---
 .../authorization/FunctionAuthorizationIT.java     | 294 +++++++++++++++++++++
 .../AuthorizationExpressionConstants.java          |  15 ++
 .../AuthorizationExpressionConverter.java          |  22 +-
 .../server/web/rest/FunctionOperations.java        |  96 +++++--
 4 files changed, 406 insertions(+), 21 deletions(-)

diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/FunctionAuthorizationIT.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/FunctionAuthorizationIT.java
new file mode 100644
index 0000000000..6493e1278e
--- /dev/null
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/FunctionAuthorizationIT.java
@@ -0,0 +1,294 @@
+/*
+ * 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.client.integration.test.authorization;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.SecurableObjects;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionCatalog;
+import org.apache.gravitino.function.FunctionChange;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionDefinitions;
+import org.apache.gravitino.function.FunctionImpl;
+import org.apache.gravitino.function.FunctionImpls;
+import org.apache.gravitino.function.FunctionParam;
+import org.apache.gravitino.function.FunctionParams;
+import org.apache.gravitino.function.FunctionType;
+import org.apache.gravitino.rel.types.Types;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+@Tag("gravitino-docker-test")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class FunctionAuthorizationIT extends BaseRestApiAuthorizationIT {
+
+  private static final String CATALOG = "catalog";
+
+  private static final String SCHEMA = "schema";
+
+  private static final String ROLE = "role";
+
+  @BeforeAll
+  public void startIntegrationTest() throws Exception {
+    super.startIntegrationTest();
+    client
+        .loadMetalake(METALAKE)
+        .createCatalog(CATALOG, Catalog.Type.MODEL, "model", "comment", new 
HashMap<>())
+        .asSchemas()
+        .createSchema(SCHEMA, "test", new HashMap<>());
+
+    // grant tester USE_CATALOG so the normal user can at least reach the 
catalog
+    List<SecurableObject> securableObjects = new ArrayList<>();
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    SecurableObject catalogObject =
+        SecurableObjects.ofCatalog(CATALOG, 
ImmutableList.of(Privileges.UseCatalog.allow()));
+    securableObjects.add(catalogObject);
+    metalake.createRole(ROLE, new HashMap<>(), securableObjects);
+    metalake.grantRolesToUser(ImmutableList.of(ROLE), NORMAL_USER);
+  }
+
+  @Test
+  @Order(1)
+  public void testRegisterFunction() {
+    FunctionCatalog functionCatalog =
+        client.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+    functionCatalog.registerFunction(
+        NameIdentifier.of(SCHEMA, "func1"), "", FunctionType.SCALAR, true, 
newDefinitions());
+
+    FunctionCatalog normalUserCatalog =
+        
normalUserClient.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+
+    // normal user cannot register without REGISTER_FUNCTION
+    assertThrows(
+        ForbiddenException.class,
+        () ->
+            normalUserCatalog.registerFunction(
+                NameIdentifier.of(SCHEMA, "func2"),
+                "",
+                FunctionType.SCALAR,
+                true,
+                newDefinitions()));
+
+    // normal user cannot list functions without USE_SCHEMA privilege
+    assertThrows(
+        ForbiddenException.class, () -> 
normalUserCatalog.listFunctions(Namespace.of(SCHEMA)));
+
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+
+    // Grant REGISTER_FUNCTION + USE_SCHEMA on the catalog
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(null, CATALOG, MetadataObject.Type.CATALOG),
+        ImmutableList.of(Privileges.UseSchema.allow(), 
Privileges.RegisterFunction.allow()));
+    normalUserCatalog.registerFunction(
+        NameIdentifier.of(SCHEMA, "func2"), "", FunctionType.SCALAR, true, 
newDefinitions());
+
+    // Revoke REGISTER_FUNCTION — normal user should no longer register
+    metalake.revokePrivilegesFromRole(
+        ROLE,
+        MetadataObjects.of(null, CATALOG, MetadataObject.Type.CATALOG),
+        ImmutableSet.of(Privileges.RegisterFunction.allow()));
+    assertThrows(
+        ForbiddenException.class,
+        () ->
+            normalUserCatalog.registerFunction(
+                NameIdentifier.of(SCHEMA, "func3"),
+                "",
+                FunctionType.SCALAR,
+                true,
+                newDefinitions()));
+  }
+
+  @Test
+  @Order(2)
+  public void testListFunction() {
+    FunctionCatalog normalUserCatalog =
+        
normalUserClient.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+    assertFunctionNames(normalUserCatalog.listFunctions(Namespace.of(SCHEMA)), 
"func2");
+
+    FunctionCatalog adminCatalog =
+        client.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+    assertFunctionNames(adminCatalog.listFunctions(Namespace.of(SCHEMA)), 
"func1", "func2");
+
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(CATALOG, SCHEMA, MetadataObject.Type.SCHEMA),
+        ImmutableList.of(Privileges.ExecuteFunction.allow()));
+    assertFunctionNames(normalUserCatalog.listFunctions(Namespace.of(SCHEMA)), 
"func1", "func2");
+
+    metalake.revokePrivilegesFromRole(
+        ROLE,
+        MetadataObjects.of(CATALOG, SCHEMA, MetadataObject.Type.SCHEMA),
+        ImmutableSet.of(Privileges.ExecuteFunction.allow()));
+    assertFunctionNames(normalUserCatalog.listFunctions(Namespace.of(SCHEMA)), 
"func2");
+  }
+
+  @Test
+  @Order(3)
+  public void testGetFunction() {
+    FunctionCatalog normalUserCatalog =
+        
normalUserClient.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+    // cannot load func1 without EXECUTE_FUNCTION / MODIFY_FUNCTION / ownership
+    assertThrows(
+        ForbiddenException.class,
+        () -> normalUserCatalog.getFunction(NameIdentifier.of(SCHEMA, 
"func1")));
+
+    // Grant EXECUTE_FUNCTION on schema
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(CATALOG, SCHEMA, MetadataObject.Type.SCHEMA),
+        ImmutableList.of(Privileges.ExecuteFunction.allow()));
+    Function func = normalUserCatalog.getFunction(NameIdentifier.of(SCHEMA, 
"func1"));
+    assertEquals("func1", func.name());
+
+    // revoke and confirm
+    metalake.revokePrivilegesFromRole(
+        ROLE,
+        MetadataObjects.of(CATALOG, SCHEMA, MetadataObject.Type.SCHEMA),
+        ImmutableSet.of(Privileges.ExecuteFunction.allow()));
+    assertThrows(
+        ForbiddenException.class,
+        () -> normalUserCatalog.getFunction(NameIdentifier.of(SCHEMA, 
"func1")));
+
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(
+            ImmutableList.of(CATALOG, SCHEMA, "func1"), 
MetadataObject.Type.FUNCTION),
+        ImmutableList.of(Privileges.ModifyFunction.allow()));
+    func = normalUserCatalog.getFunction(NameIdentifier.of(SCHEMA, "func1"));
+    assertEquals("func1", func.name());
+
+    metalake.revokePrivilegesFromRole(
+        ROLE,
+        MetadataObjects.of(
+            ImmutableList.of(CATALOG, SCHEMA, "func1"), 
MetadataObject.Type.FUNCTION),
+        ImmutableSet.of(Privileges.ModifyFunction.allow()));
+    assertThrows(
+        ForbiddenException.class,
+        () -> normalUserCatalog.getFunction(NameIdentifier.of(SCHEMA, 
"func1")));
+  }
+
+  @Test
+  @Order(4)
+  public void testAlterFunction() {
+    FunctionCatalog normalUserCatalog =
+        
normalUserClient.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+    // cannot alter func1 (owned by admin) without MODIFY_FUNCTION or ownership
+    assertThrows(
+        ForbiddenException.class,
+        () ->
+            normalUserCatalog.alterFunction(
+                NameIdentifier.of(SCHEMA, "func1"), 
FunctionChange.updateComment("new-comment")));
+
+    // Grant MODIFY_FUNCTION at function level
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(
+            ImmutableList.of(CATALOG, SCHEMA, "func1"), 
MetadataObject.Type.FUNCTION),
+        ImmutableList.of(Privileges.ModifyFunction.allow()));
+    Function altered =
+        normalUserCatalog.alterFunction(
+            NameIdentifier.of(SCHEMA, "func1"), 
FunctionChange.updateComment("new-comment"));
+    assertEquals("new-comment", altered.comment());
+
+    // Revoke MODIFY_FUNCTION
+    metalake.revokePrivilegesFromRole(
+        ROLE,
+        MetadataObjects.of(
+            ImmutableList.of(CATALOG, SCHEMA, "func1"), 
MetadataObject.Type.FUNCTION),
+        ImmutableSet.of(Privileges.ModifyFunction.allow()));
+    assertThrows(
+        ForbiddenException.class,
+        () ->
+            normalUserCatalog.alterFunction(
+                NameIdentifier.of(SCHEMA, "func1"),
+                FunctionChange.updateComment("another-comment")));
+  }
+
+  @Test
+  @Order(5)
+  public void testDropFunction() {
+    FunctionCatalog adminCatalog =
+        client.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+    adminCatalog.registerFunction(
+        NameIdentifier.of(SCHEMA, "func_to_drop"), "", FunctionType.SCALAR, 
true, newDefinitions());
+
+    FunctionCatalog normalUserCatalog =
+        
normalUserClient.loadMetalake(METALAKE).loadCatalog(CATALOG).asFunctionCatalog();
+    // MODIFY_FUNCTION alone does not grant DROP — only OWNER can drop
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(
+            ImmutableList.of(CATALOG, SCHEMA, "func_to_drop"), 
MetadataObject.Type.FUNCTION),
+        ImmutableList.of(Privileges.ModifyFunction.allow()));
+    assertThrows(
+        ForbiddenException.class,
+        () -> normalUserCatalog.dropFunction(NameIdentifier.of(SCHEMA, 
"func_to_drop")));
+
+    // Normal user can drop functions they own (func2 registered by them in 
test 1)
+    assertEquals(true, 
normalUserCatalog.dropFunction(NameIdentifier.of(SCHEMA, "func2")));
+
+    // Admin can drop func1 and func_to_drop
+    assertEquals(true, adminCatalog.dropFunction(NameIdentifier.of(SCHEMA, 
"func1")));
+    assertEquals(true, adminCatalog.dropFunction(NameIdentifier.of(SCHEMA, 
"func_to_drop")));
+  }
+
+  private static FunctionDefinition[] newDefinitions() {
+    FunctionParam param = FunctionParams.of("x", Types.IntegerType.get());
+    FunctionImpl impl = FunctionImpls.ofSql(FunctionImpl.RuntimeType.SPARK, 
"SELECT x + 1");
+    return new FunctionDefinition[] {
+      FunctionDefinitions.of(
+          new FunctionParam[] {param}, Types.IntegerType.get(), new 
FunctionImpl[] {impl})
+    };
+  }
+
+  private static void assertFunctionNames(NameIdentifier[] functions, 
String... expectedNames) {
+    List<String> actualNames =
+        
Arrays.stream(functions).map(NameIdentifier::name).sorted().collect(Collectors.toList());
+    List<String> expectedFunctionNames = Arrays.asList(expectedNames);
+    Collections.sort(expectedFunctionNames);
+    assertEquals(expectedFunctionNames, actualNames);
+  }
+}
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 d145b2c9b8..6e72401720 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
@@ -75,6 +75,21 @@ public class AuthorizationExpressionConstants {
   public static final String FILTER_MODEL_AUTHORIZATION_EXPRESSION =
       "ANY(OWNER, METALAKE, CATALOG, SCHEMA, MODEL) || ANY_USE_MODEL";
 
+  public static final String LOAD_FUNCTION_AUTHORIZATION_EXPRESSION =
+      """
+            ANY(OWNER, METALAKE, CATALOG) ||
+             SCHEMA_OWNER_WITH_USE_CATALOG ||
+              ANY_USE_CATALOG && ANY_USE_SCHEMA
+              && (FUNCTION::OWNER || ANY_EXECUTE_FUNCTION || 
ANY_MODIFY_FUNCTION)
+                  """;
+
+  public static final String FILTER_FUNCTION_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY(OWNER, METALAKE, CATALOG, SCHEMA, FUNCTION) ||
+                  ANY_EXECUTE_FUNCTION ||
+                  ANY_MODIFY_FUNCTION
+                  """;
+
   public static final String LOAD_VIEW_AUTHORIZATION_EXPRESSION =
       """
                   ANY(OWNER, METALAKE, CATALOG) ||
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
index 4cf4b70214..99123a162f 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java
@@ -19,6 +19,7 @@ package org.apache.gravitino.server.authorization.expression;
 
 import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.LOAD_CATALOG_AUTHORIZATION_EXPRESSION;
 import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.LOAD_FILESET_AUTHORIZATION_EXPRESSION;
+import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.LOAD_FUNCTION_AUTHORIZATION_EXPRESSION;
 import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.LOAD_JOB_AUTHORIZATION_EXPRESSION;
 import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.LOAD_JOB_TEMPLATE_AUTHORIZATION_EXPRESSION;
 import static 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants.LOAD_METALAKE_AUTHORIZATION_EXPRESSION;
@@ -182,7 +183,8 @@ public class AuthorizationExpressionConverter {
               ( entityType == 'TAG' && (%s)) ||
               ( entityType == 'JOB' && (%s)) ||
               ( entityType == 'JOB_TEMPLATE' && (%s)) ||
-              ( entityType == 'COLUMN' && (%s))
+              ( entityType == 'COLUMN' && (%s)) ||
+              ( entityType == 'FUNCTION' && (%s))
               """
             .formatted(
                 LOAD_CATALOG_AUTHORIZATION_EXPRESSION,
@@ -198,7 +200,8 @@ public class AuthorizationExpressionConverter {
                 LOAD_TAG_AUTHORIZATION_EXPRESSION,
                 LOAD_JOB_AUTHORIZATION_EXPRESSION,
                 LOAD_JOB_TEMPLATE_AUTHORIZATION_EXPRESSION,
-                LOAD_TABLE_AUTHORIZATION_EXPRESSION));
+                LOAD_TABLE_AUTHORIZATION_EXPRESSION,
+                LOAD_FUNCTION_AUTHORIZATION_EXPRESSION));
   }
 
   /**
@@ -291,6 +294,21 @@ public class AuthorizationExpressionConverter {
             "ANY_REGISTER_MODEL",
             "((ANY(REGISTER_MODEL, METALAKE, CATALOG, SCHEMA)) "
                 + "&& !(ANY(DENY_REGISTER_MODEL, METALAKE, CATALOG, 
SCHEMA)))");
+    expression =
+        expression.replaceAll(
+            "ANY_REGISTER_FUNCTION",
+            "((ANY(REGISTER_FUNCTION, METALAKE, CATALOG, SCHEMA)) "
+                + "&& !(ANY(DENY_REGISTER_FUNCTION, METALAKE, CATALOG, 
SCHEMA)))");
+    expression =
+        expression.replaceAll(
+            "ANY_EXECUTE_FUNCTION",
+            "((ANY(EXECUTE_FUNCTION, METALAKE, CATALOG, SCHEMA, FUNCTION)) "
+                + "&& !(ANY(DENY_EXECUTE_FUNCTION, METALAKE, CATALOG, SCHEMA, 
FUNCTION)))");
+    expression =
+        expression.replaceAll(
+            "ANY_MODIFY_FUNCTION",
+            "((ANY(MODIFY_FUNCTION, METALAKE, CATALOG, SCHEMA, FUNCTION)) "
+                + "&& !(ANY(DENY_MODIFY_FUNCTION, METALAKE, CATALOG, SCHEMA, 
FUNCTION)))");
     expression =
         expression.replaceAll(
             "ANY_CREATE_TOPIC",
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/FunctionOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/FunctionOperations.java
index b4b6c4c7a8..70b7b9e2ae 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/FunctionOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/FunctionOperations.java
@@ -34,6 +34,8 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.QueryParam;
 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.FunctionDispatcher;
@@ -51,6 +53,10 @@ import org.apache.gravitino.function.Function;
 import org.apache.gravitino.function.FunctionChange;
 import org.apache.gravitino.function.FunctionDefinition;
 import org.apache.gravitino.metrics.MetricNames;
+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;
@@ -72,15 +78,18 @@ public class FunctionOperations {
     this.dispatcher = dispatcher;
   }
 
-  // TODO: Add authorization support for function operations
   @GET
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "list-function." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "list-function", absolute = true)
+  @AuthorizationExpression(
+      expression = 
AuthorizationExpressionConstants.LOAD_SCHEMA_AUTHORIZATION_EXPRESSION,
+      accessMetadataType = MetadataObject.Type.SCHEMA)
   public Response listFunctions(
-      @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,
       @QueryParam("details") @DefaultValue("false") boolean details) {
     try {
       LOG.info("Received list functions request for schema: {}.{}.{}", 
metalake, catalog, schema);
@@ -90,6 +99,13 @@ public class FunctionOperations {
             Namespace namespace = NamespaceUtil.ofFunction(metalake, catalog, 
schema);
             if (!details) {
               NameIdentifier[] identifiers = 
dispatcher.listFunctions(namespace);
+              identifiers = identifiers == null ? new NameIdentifier[0] : 
identifiers;
+              identifiers =
+                  MetadataAuthzHelper.filterByExpression(
+                      metalake,
+                      
AuthorizationExpressionConstants.FILTER_FUNCTION_AUTHORIZATION_EXPRESSION,
+                      Entity.EntityType.FUNCTION,
+                      identifiers);
               LOG.info(
                   "List {} function names under schema: {}.{}.{}",
                   identifiers.length,
@@ -100,6 +116,14 @@ public class FunctionOperations {
             }
 
             Function[] functions = dispatcher.listFunctionInfos(namespace);
+            functions = functions == null ? new Function[0] : functions;
+            functions =
+                MetadataAuthzHelper.filterByExpression(
+                    metalake,
+                    
AuthorizationExpressionConstants.FILTER_FUNCTION_AUTHORIZATION_EXPRESSION,
+                    Entity.EntityType.FUNCTION,
+                    functions,
+                    f -> NameIdentifier.of(namespace, f.name()));
             FunctionDTO[] functionDTOs =
                 Arrays.stream(functions)
                     .map(DTOConverters::toDTO)
@@ -122,10 +146,19 @@ public class FunctionOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "register-function." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "register-function", absolute = true)
+  @AuthorizationExpression(
+      expression =
+          """
+                      ANY(OWNER, METALAKE, CATALOG) ||
+                      SCHEMA_OWNER_WITH_USE_CATALOG ||
+                      ANY_USE_CATALOG && ANY_USE_SCHEMA && 
ANY_REGISTER_FUNCTION
+                      """,
+      accessMetadataType = MetadataObject.Type.SCHEMA)
   public Response registerFunction(
-      @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,
       FunctionRegisterRequest request) {
     LOG.info(
         "Received register function request: {}.{}.{}.{}",
@@ -170,11 +203,16 @@ public class FunctionOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "get-function." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "get-function", absolute = true)
+  @AuthorizationExpression(
+      expression = 
AuthorizationExpressionConstants.LOAD_FUNCTION_AUTHORIZATION_EXPRESSION,
+      accessMetadataType = MetadataObject.Type.FUNCTION)
   public Response getFunction(
-      @PathParam("metalake") String metalake,
-      @PathParam("catalog") String catalog,
-      @PathParam("schema") String schema,
-      @PathParam("function") String function) {
+      @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("function") @AuthorizationMetadata(type = 
Entity.EntityType.FUNCTION)
+          String function) {
     LOG.info("Received get function request: {}.{}.{}.{}", metalake, catalog, 
schema, function);
     try {
       return Utils.doAs(
@@ -197,11 +235,21 @@ public class FunctionOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "alter-function." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "alter-function", absolute = true)
+  @AuthorizationExpression(
+      expression =
+          """
+                      ANY(OWNER, METALAKE, CATALOG) ||
+                      SCHEMA_OWNER_WITH_USE_CATALOG ||
+                      ANY_USE_CATALOG && ANY_USE_SCHEMA && (FUNCTION::OWNER || 
ANY_MODIFY_FUNCTION)
+                      """,
+      accessMetadataType = MetadataObject.Type.FUNCTION)
   public Response alterFunction(
-      @PathParam("metalake") String metalake,
-      @PathParam("catalog") String catalog,
-      @PathParam("schema") String schema,
-      @PathParam("function") String function,
+      @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("function") @AuthorizationMetadata(type = 
Entity.EntityType.FUNCTION)
+          String function,
       FunctionUpdatesRequest request) {
     LOG.info("Received alter function request: {}.{}.{}.{}", metalake, 
catalog, schema, function);
     try {
@@ -230,11 +278,21 @@ public class FunctionOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "drop-function." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "drop-function", absolute = true)
+  @AuthorizationExpression(
+      expression =
+          """
+                      ANY(OWNER, METALAKE, CATALOG) ||
+                      SCHEMA_OWNER_WITH_USE_CATALOG ||
+                      ANY_USE_CATALOG && ANY_USE_SCHEMA && FUNCTION::OWNER
+                      """,
+      accessMetadataType = MetadataObject.Type.FUNCTION)
   public Response dropFunction(
-      @PathParam("metalake") String metalake,
-      @PathParam("catalog") String catalog,
-      @PathParam("schema") String schema,
-      @PathParam("function") String function) {
+      @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("function") @AuthorizationMetadata(type = 
Entity.EntityType.FUNCTION)
+          String function) {
     LOG.info("Received drop function request: {}.{}.{}.{}", metalake, catalog, 
schema, function);
     try {
       return Utils.doAs(

Reply via email to