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 217b76e25a [#9535] feat(api): introduce FUNCTION metadata object type 
and function privileges (#10811)
217b76e25a is described below

commit 217b76e25a9ddcf4b96b1ae459bb16914e761636
Author: mchades <[email protected]>
AuthorDate: Thu Apr 23 15:08:54 2026 +0800

    [#9535] feat(api): introduce FUNCTION metadata object type and function 
privileges (#10811)
    
    ### What changes were proposed in this pull request?
    
    Add the `FUNCTION` metadata object type and three new function-level
    privileges
    (`REGISTER_FUNCTION`, `EXECUTE_FUNCTION`, `MODIFY_FUNCTION`) to the
    Gravitino
    authorization API, following the design in
    `design-docs/gravitino-function-privilege.md`.
    
    Key changes:
    - `MetadataObject.Type.FUNCTION` — new FUNCTION type
    - `Privilege.Name.REGISTER_FUNCTION/EXECUTE_FUNCTION/MODIFY_FUNCTION` —
    three new privilege names
    - `Privileges.RegisterFunction/ExecuteFunction/ModifyFunction` —
    corresponding privilege classes with correct supported-type bindings
    - `SecurableObjects.ofFunction()` — convenience factory for function
    securable objects
    - `MetadataObjects` — FUNCTION added to valid three-level name types
    
    ### Why are the changes needed?
    
    Gravitino manages user-defined functions (UDFs) but provides no access
    control at the function level. This PR is the API foundation for
    end-to-end function privilege enforcement.
    
    Fix: #9535
    
    ### Does this PR introduce _any_ user-facing change?
    
    - New public API types and classes: `MetadataObject.Type.FUNCTION`,
    `Privilege.Name.REGISTER_FUNCTION`, `EXECUTE_FUNCTION`,
    `MODIFY_FUNCTION`
    - New `Privileges.RegisterFunction`, `ExecuteFunction`, `ModifyFunction`
    classes
    - New `SecurableObjects.ofFunction(...)` factory method
    
    ### How was this patch tested?
    
    - `TestMetadataObjects.testFunctionObject` — validates FUNCTION metadata
    object construction
    - `TestSecurableObjects` — new entries for `canBindTo` and `manageGrants
    FUNCTION` binding
    - All unit tests pass: `./gradlew :api:test -PskipITs`
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 .../java/org/apache/gravitino/MetadataObject.java  |   4 +-
 .../java/org/apache/gravitino/MetadataObjects.java |   4 +-
 .../apache/gravitino/authorization/Privilege.java  |   8 +-
 .../apache/gravitino/authorization/Privileges.java | 119 ++++++++++++++++++++-
 .../gravitino/authorization/SecurableObjects.java  |  16 +++
 .../org/apache/gravitino/TestMetadataObjects.java  |  43 ++++++++
 .../authorization/TestSecurableObjects.java        |  37 +++++++
 design-docs/gravitino-function-privilege.md        |   4 +-
 8 files changed, 229 insertions(+), 6 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/MetadataObject.java 
b/api/src/main/java/org/apache/gravitino/MetadataObject.java
index 84ec879e81..97a8525284 100644
--- a/api/src/main/java/org/apache/gravitino/MetadataObject.java
+++ b/api/src/main/java/org/apache/gravitino/MetadataObject.java
@@ -73,7 +73,9 @@ public interface MetadataObject {
     /** A job represents a data processing task in Gravitino. */
     JOB,
     /** A job template represents a reusable template for creating jobs in 
Gravitino. */
-    JOB_TEMPLATE;
+    JOB_TEMPLATE,
+    /** A function represents a user-defined function registered in Gravitino. 
*/
+    FUNCTION;
   }
 
   /**
diff --git a/api/src/main/java/org/apache/gravitino/MetadataObjects.java 
b/api/src/main/java/org/apache/gravitino/MetadataObjects.java
index 9bce690183..57bf2fc252 100644
--- a/api/src/main/java/org/apache/gravitino/MetadataObjects.java
+++ b/api/src/main/java/org/apache/gravitino/MetadataObjects.java
@@ -59,7 +59,8 @@ public class MetadataObjects {
           MetadataObject.Type.TABLE,
           MetadataObject.Type.VIEW,
           MetadataObject.Type.TOPIC,
-          MetadataObject.Type.MODEL);
+          MetadataObject.Type.MODEL,
+          MetadataObject.Type.FUNCTION);
 
   private static final Set<MetadataObject.Type> VALID_FOUR_LEVEL_NAME_TYPES =
       Sets.newHashSet(MetadataObject.Type.COLUMN);
@@ -151,6 +152,7 @@ public class MetadataObjects {
       case FILESET:
       case TOPIC:
       case MODEL:
+      case FUNCTION:
         parentType = MetadataObject.Type.SCHEMA;
         break;
       case SCHEMA:
diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/Privilege.java 
b/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
index 3aac7af18d..69e1392d69 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
@@ -145,7 +145,13 @@ public interface Privilege {
     /** The privilege to create a view. */
     CREATE_VIEW(0L, 1L << 28),
     /** The privilege to select data from a view. */
-    SELECT_VIEW(0L, 1L << 29);
+    SELECT_VIEW(0L, 1L << 29),
+    /** The privilege to register a function. */
+    REGISTER_FUNCTION(0L, 1L << 30),
+    /** The privilege to execute (invoke) a function. */
+    EXECUTE_FUNCTION(0L, 1L << 31),
+    /** The privilege to alter a function's metadata. */
+    MODIFY_FUNCTION(0L, 1L << 32);
 
     private final long highBits;
     private final long lowBits;
diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/Privileges.java 
b/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
index 29b273189d..c44d251a10 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
@@ -64,6 +64,13 @@ public class Privileges {
           MetadataObject.Type.SCHEMA,
           MetadataObject.Type.VIEW);
 
+  private static final Set<MetadataObject.Type> FUNCTION_SUPPORTED_TYPES =
+      Sets.immutableEnumSet(
+          MetadataObject.Type.METALAKE,
+          MetadataObject.Type.CATALOG,
+          MetadataObject.Type.SCHEMA,
+          MetadataObject.Type.FUNCTION);
+
   /**
    * Object types that {@link ManageGrants} can be bound to.
    *
@@ -79,7 +86,8 @@ public class Privileges {
           MetadataObject.Type.VIEW,
           MetadataObject.Type.TOPIC,
           MetadataObject.Type.FILESET,
-          MetadataObject.Type.MODEL);
+          MetadataObject.Type.MODEL,
+          MetadataObject.Type.FUNCTION);
 
   /**
    * Returns the Privilege with allow condition from the string representation.
@@ -190,6 +198,14 @@ public class Privileges {
       case SELECT_VIEW:
         return SelectView.allow();
 
+        // Function
+      case REGISTER_FUNCTION:
+        return RegisterFunction.allow();
+      case EXECUTE_FUNCTION:
+        return ExecuteFunction.allow();
+      case MODIFY_FUNCTION:
+        return ModifyFunction.allow();
+
       default:
         throw new IllegalArgumentException("Doesn't support the privilege: " + 
name);
     }
@@ -304,6 +320,14 @@ public class Privileges {
       case SELECT_VIEW:
         return SelectView.deny();
 
+        // Function
+      case REGISTER_FUNCTION:
+        return RegisterFunction.deny();
+      case EXECUTE_FUNCTION:
+        return ExecuteFunction.deny();
+      case MODIFY_FUNCTION:
+        return ModifyFunction.deny();
+
       default:
         throw new IllegalArgumentException("Doesn't support the privilege: " + 
name);
     }
@@ -1343,4 +1367,97 @@ public class Privileges {
       return VIEW_SUPPORTED_TYPES.contains(type);
     }
   }
+
+  /** The privilege to register a function. */
+  public static class RegisterFunction extends 
GenericPrivilege<RegisterFunction> {
+    private static final RegisterFunction ALLOW_INSTANCE =
+        new RegisterFunction(Condition.ALLOW, Name.REGISTER_FUNCTION);
+    private static final RegisterFunction DENY_INSTANCE =
+        new RegisterFunction(Condition.DENY, Name.REGISTER_FUNCTION);
+
+    private RegisterFunction(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /**
+     * @return The instance with allow condition of the privilege.
+     */
+    public static RegisterFunction allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /**
+     * @return The instance with deny condition of the privilege.
+     */
+    public static RegisterFunction deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return SCHEMA_SUPPORTED_TYPES.contains(type);
+    }
+  }
+
+  /** The privilege to execute (invoke) a function and view its metadata. */
+  public static class ExecuteFunction extends 
GenericPrivilege<ExecuteFunction> {
+    private static final ExecuteFunction ALLOW_INSTANCE =
+        new ExecuteFunction(Condition.ALLOW, Name.EXECUTE_FUNCTION);
+    private static final ExecuteFunction DENY_INSTANCE =
+        new ExecuteFunction(Condition.DENY, Name.EXECUTE_FUNCTION);
+
+    private ExecuteFunction(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /**
+     * @return The instance with allow condition of the privilege.
+     */
+    public static ExecuteFunction allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /**
+     * @return The instance with deny condition of the privilege.
+     */
+    public static ExecuteFunction deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return FUNCTION_SUPPORTED_TYPES.contains(type);
+    }
+  }
+
+  /** The privilege to alter a function's metadata. */
+  public static class ModifyFunction extends GenericPrivilege<ModifyFunction> {
+    private static final ModifyFunction ALLOW_INSTANCE =
+        new ModifyFunction(Condition.ALLOW, Name.MODIFY_FUNCTION);
+    private static final ModifyFunction DENY_INSTANCE =
+        new ModifyFunction(Condition.DENY, Name.MODIFY_FUNCTION);
+
+    private ModifyFunction(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /**
+     * @return The instance with allow condition of the privilege.
+     */
+    public static ModifyFunction allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /**
+     * @return The instance with deny condition of the privilege.
+     */
+    public static ModifyFunction deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return FUNCTION_SUPPORTED_TYPES.contains(type);
+    }
+  }
 }
diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java 
b/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
index 970ae683ba..dd69a87427 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
@@ -154,6 +154,22 @@ public class SecurableObjects {
     return of(MetadataObject.Type.MODEL, names, privileges);
   }
 
+  /**
+   * Create the function {@link SecurableObject} with the given securable 
schema object, function
+   * name and privileges.
+   *
+   * @param schema The schema securable object
+   * @param function The function name
+   * @param privileges The privileges of the function
+   * @return The created function {@link SecurableObject}
+   */
+  public static SecurableObject ofFunction(
+      SecurableObject schema, String function, List<Privilege> privileges) {
+    List<String> names = 
Lists.newArrayList(DOT_SPLITTER.splitToList(schema.fullName()));
+    names.add(function);
+    return of(MetadataObject.Type.FUNCTION, names, privileges);
+  }
+
   /**
    * Create the tag {@link SecurableObject} with the given tag name and 
privileges.
    *
diff --git a/api/src/test/java/org/apache/gravitino/TestMetadataObjects.java 
b/api/src/test/java/org/apache/gravitino/TestMetadataObjects.java
index ed41561033..bb01469dbe 100644
--- a/api/src/test/java/org/apache/gravitino/TestMetadataObjects.java
+++ b/api/src/test/java/org/apache/gravitino/TestMetadataObjects.java
@@ -179,4 +179,47 @@ public class TestMetadataObjects {
         () ->
             MetadataObjects.of(Lists.newArrayList("catalog", "schema"), 
MetadataObject.Type.VIEW));
   }
+
+  @Test
+  public void testFunctionObject() {
+    MetadataObject functionObject =
+        MetadataObjects.of("catalog.schema", "func1", 
MetadataObject.Type.FUNCTION);
+    Assertions.assertEquals("catalog.schema", functionObject.parent());
+    Assertions.assertEquals("func1", functionObject.name());
+    Assertions.assertEquals(MetadataObject.Type.FUNCTION, 
functionObject.type());
+    Assertions.assertEquals("catalog.schema.func1", functionObject.fullName());
+
+    MetadataObject functionObject2 =
+        MetadataObjects.of(
+            Lists.newArrayList("catalog", "schema", "func2"), 
MetadataObject.Type.FUNCTION);
+    Assertions.assertEquals("catalog.schema", functionObject2.parent());
+    Assertions.assertEquals("func2", functionObject2.name());
+    Assertions.assertEquals(MetadataObject.Type.FUNCTION, 
functionObject2.type());
+    Assertions.assertEquals("catalog.schema.func2", 
functionObject2.fullName());
+
+    MetadataObject functionObject3 =
+        MetadataObjects.parse("catalog.schema.func3", 
MetadataObject.Type.FUNCTION);
+    Assertions.assertEquals("catalog.schema", functionObject3.parent());
+    Assertions.assertEquals("func3", functionObject3.name());
+    Assertions.assertEquals(MetadataObject.Type.FUNCTION, 
functionObject3.type());
+    Assertions.assertEquals("catalog.schema.func3", 
functionObject3.fullName());
+
+    // Test parent
+    MetadataObject parent = MetadataObjects.parent(functionObject);
+    Assertions.assertEquals("catalog.schema", parent.fullName());
+    Assertions.assertEquals("catalog", parent.parent());
+    Assertions.assertEquals("schema", parent.name());
+    Assertions.assertEquals(MetadataObject.Type.SCHEMA, parent.type());
+
+    // Test incomplete name
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> MetadataObjects.parse("func1", MetadataObject.Type.FUNCTION));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> MetadataObjects.parse("catalog", MetadataObject.Type.FUNCTION));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> MetadataObjects.parse("catalog.schema", 
MetadataObject.Type.FUNCTION));
+  }
 }
diff --git 
a/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
 
b/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
index f0bf9b90dc..22050f6c80 100644
--- 
a/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
+++ 
b/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
@@ -211,6 +211,9 @@ public class TestSecurableObjects {
     Privilege useJobTemplate = Privileges.UseJobTemplate.allow();
     Privilege createView = Privileges.CreateView.allow();
     Privilege selectView = Privileges.SelectView.allow();
+    Privilege registerFunction = Privileges.RegisterFunction.allow();
+    Privilege executeFunction = Privileges.ExecuteFunction.allow();
+    Privilege modifyFunction = Privileges.ModifyFunction.allow();
 
     // Test create catalog
     
Assertions.assertTrue(createCatalog.canBindTo(MetadataObject.Type.METALAKE));
@@ -381,6 +384,7 @@ public class TestSecurableObjects {
     Assertions.assertTrue(manageGrants.canBindTo(MetadataObject.Type.FILESET));
     Assertions.assertTrue(manageGrants.canBindTo(MetadataObject.Type.VIEW));
     Assertions.assertTrue(manageGrants.canBindTo(MetadataObject.Type.MODEL));
+    
Assertions.assertTrue(manageGrants.canBindTo(MetadataObject.Type.FUNCTION));
     Assertions.assertFalse(manageGrants.canBindTo(MetadataObject.Type.ROLE));
     Assertions.assertFalse(manageGrants.canBindTo(MetadataObject.Type.COLUMN));
 
@@ -504,5 +508,38 @@ public class TestSecurableObjects {
     Assertions.assertFalse(selectView.canBindTo(MetadataObject.Type.ROLE));
     Assertions.assertFalse(selectView.canBindTo(MetadataObject.Type.COLUMN));
     Assertions.assertTrue(selectView.canBindTo(MetadataObject.Type.VIEW));
+
+    // Test register function
+    
Assertions.assertTrue(registerFunction.canBindTo(MetadataObject.Type.METALAKE));
+    
Assertions.assertTrue(registerFunction.canBindTo(MetadataObject.Type.CATALOG));
+    
Assertions.assertTrue(registerFunction.canBindTo(MetadataObject.Type.SCHEMA));
+    
Assertions.assertFalse(registerFunction.canBindTo(MetadataObject.Type.TABLE));
+    
Assertions.assertFalse(registerFunction.canBindTo(MetadataObject.Type.TOPIC));
+    
Assertions.assertFalse(registerFunction.canBindTo(MetadataObject.Type.FILESET));
+    
Assertions.assertFalse(registerFunction.canBindTo(MetadataObject.Type.ROLE));
+    
Assertions.assertFalse(registerFunction.canBindTo(MetadataObject.Type.COLUMN));
+    
Assertions.assertFalse(registerFunction.canBindTo(MetadataObject.Type.FUNCTION));
+
+    // Test execute function
+    
Assertions.assertTrue(executeFunction.canBindTo(MetadataObject.Type.METALAKE));
+    
Assertions.assertTrue(executeFunction.canBindTo(MetadataObject.Type.CATALOG));
+    
Assertions.assertTrue(executeFunction.canBindTo(MetadataObject.Type.SCHEMA));
+    
Assertions.assertTrue(executeFunction.canBindTo(MetadataObject.Type.FUNCTION));
+    
Assertions.assertFalse(executeFunction.canBindTo(MetadataObject.Type.TABLE));
+    
Assertions.assertFalse(executeFunction.canBindTo(MetadataObject.Type.TOPIC));
+    
Assertions.assertFalse(executeFunction.canBindTo(MetadataObject.Type.FILESET));
+    
Assertions.assertFalse(executeFunction.canBindTo(MetadataObject.Type.ROLE));
+    
Assertions.assertFalse(executeFunction.canBindTo(MetadataObject.Type.COLUMN));
+
+    // Test modify function
+    
Assertions.assertTrue(modifyFunction.canBindTo(MetadataObject.Type.METALAKE));
+    
Assertions.assertTrue(modifyFunction.canBindTo(MetadataObject.Type.CATALOG));
+    
Assertions.assertTrue(modifyFunction.canBindTo(MetadataObject.Type.SCHEMA));
+    
Assertions.assertTrue(modifyFunction.canBindTo(MetadataObject.Type.FUNCTION));
+    
Assertions.assertFalse(modifyFunction.canBindTo(MetadataObject.Type.TABLE));
+    
Assertions.assertFalse(modifyFunction.canBindTo(MetadataObject.Type.TOPIC));
+    
Assertions.assertFalse(modifyFunction.canBindTo(MetadataObject.Type.FILESET));
+    Assertions.assertFalse(modifyFunction.canBindTo(MetadataObject.Type.ROLE));
+    
Assertions.assertFalse(modifyFunction.canBindTo(MetadataObject.Type.COLUMN));
   }
 }
diff --git a/design-docs/gravitino-function-privilege.md 
b/design-docs/gravitino-function-privilege.md
index c9634df93e..c6c703b577 100644
--- a/design-docs/gravitino-function-privilege.md
+++ b/design-docs/gravitino-function-privilege.md
@@ -34,7 +34,7 @@ The existing Gravitino access control framework covers 
catalogs, schemas, tables
 
 1. **Integrate with Existing Access Control Framework**: Define function 
privilege types that follow established Gravitino naming conventions and 
privilege inheritance patterns.
 
-2. **Function Visibility Control**: Users should only see functions they have 
privileges on. `listFunctions` and `getFunction` should filter results based on 
user permissions, following the "can't see what you can't execute" pattern 
found across all surveyed systems.
+2. **Function Visibility Control**: Users should only see functions they have 
privileges on. `listFunctions` and `getFunction` should filter results based on 
user permissions, consistent with how tables and filesets handle visibility.
 
 3. **Ownership Tracking**: Functions should have owners, set automatically on 
registration and manageable through Gravitino's existing ownership mechanism.
 
@@ -106,7 +106,7 @@ This is consistent with tables, filesets, and other 
schema-scoped objects. Funct
 
 ### Visibility Control
 
-Function visibility follows the "can't see what you can't execute" pattern 
observed across all surveyed systems:
+Function visibility follows the same pattern as tables and filesets — users 
can only see functions they have at least one operational privilege on:
 
 1. **`listFunctions`**
    - Requires `USE_CATALOG` + `USE_SCHEMA` at the endpoint level to access the 
schema (consistent with `listTables`).

Reply via email to