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`).