Repository: apache/gravitino
Pull request #6820: [6346] feat(authz): Add privilege support for model
Author: login: roryqi
Created: 2025-04-02T08:11:41Z | Merged: 2025-04-07T16:43:08Z

### What changes were proposed in this pull request?

 Add privilege support for model

### Why are the changes needed?

Fix: #6346

### Does this PR introduce _any_ user-facing change?

Added the doucments

### How was this patch tested?

UT.


Diff:
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 866cf7d23a2..5515e45fbdc 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privilege.java
@@ -84,7 +84,13 @@ enum Name {
     /** The privilege to create a role */
     CREATE_ROLE(0L, 1L << 16),
     /** The privilege to grant or revoke a role for the user or the group. */
-    MANAGE_GRANTS(0L, 1L << 17);
+    MANAGE_GRANTS(0L, 1L << 17),
+    /** The privilege to create a model */
+    CREATE_MODEL(0L, 1L << 18),
+    /** The privilege to create a model version */
+    CREATE_MODEL_VERSION(0L, 1L << 19),
+    /** The privilege to view the metadata of the model and download all the model versions */
+    USE_MODEL(0L, 1L << 20);
 
     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 b0f9e8fcc43..cf3bdb0692e 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Privileges.java
@@ -32,6 +32,14 @@ public class Privileges {
           MetadataObject.Type.CATALOG,
           MetadataObject.Type.SCHEMA,
           MetadataObject.Type.TABLE);
+
+  private static final Set<MetadataObject.Type> MODEL_SUPPORTED_TYPES =
+      Sets.immutableEnumSet(
+          MetadataObject.Type.METALAKE,
+          MetadataObject.Type.CATALOG,
+          MetadataObject.Type.SCHEMA,
+          MetadataObject.Type.MODEL);
+
   private static final Set<MetadataObject.Type> TOPIC_SUPPORTED_TYPES =
       Sets.immutableEnumSet(
           MetadataObject.Type.METALAKE,
@@ -118,6 +126,14 @@ public static Privilege allow(Privilege.Name name) {
       case MANAGE_GRANTS:
         return ManageGrants.allow();
 
+        //  Model
+      case CREATE_MODEL:
+        return CreateModel.allow();
+      case CREATE_MODEL_VERSION:
+        return CreateModelVersion.allow();
+      case USE_MODEL:
+        return UseModel.allow();
+
       default:
         throw new IllegalArgumentException("Doesn't support the privilege: " + name);
     }
@@ -192,6 +208,14 @@ public static Privilege deny(Privilege.Name name) {
       case MANAGE_GRANTS:
         return ManageGrants.deny();
 
+        // Model
+      case CREATE_MODEL:
+        return CreateModel.deny();
+      case CREATE_MODEL_VERSION:
+        return CreateModelVersion.deny();
+      case USE_MODEL:
+        return UseModel.deny();
+
       default:
         throw new IllegalArgumentException("Doesn't support the privilege: " + name);
     }
@@ -703,4 +727,83 @@ public boolean canBindTo(MetadataObject.Type type) {
       return type == MetadataObject.Type.METALAKE;
     }
   }
+
+  /** The privilege to create a model */
+  public static class CreateModel extends GenericPrivilege<CreateModel> {
+    private static final CreateModel ALLOW_INSTANCE =
+        new CreateModel(Condition.ALLOW, Name.CREATE_MODEL);
+    private static final CreateModel DENY_INSTANCE =
+        new CreateModel(Condition.DENY, Name.CREATE_MODEL);
+
+    private CreateModel(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /** @return The instance with allow condition of the privilege. */
+    public static CreateModel allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /** @return The instance with deny condition of the privilege. */
+    public static CreateModel deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return SCHEMA_SUPPORTED_TYPES.contains(type);
+    }
+  }
+
+  /** The privilege to view the metadata of the model and download all the model versions */
+  public static class UseModel extends GenericPrivilege<UseModel> {
+    private static final UseModel ALLOW_INSTANCE = new UseModel(Condition.ALLOW, Name.USE_MODEL);
+    private static final UseModel DENY_INSTANCE = new UseModel(Condition.DENY, Name.USE_MODEL);
+
+    private UseModel(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /** @return The instance with allow condition of the privilege. */
+    public static UseModel allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /** @return The instance with deny condition of the privilege. */
+    public static UseModel deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return MODEL_SUPPORTED_TYPES.contains(type);
+    }
+  }
+
+  /** The privilege to create a model version */
+  public static class CreateModelVersion extends GenericPrivilege<CreateModelVersion> {
+    private static final CreateModelVersion ALLOW_INSTANCE =
+        new CreateModelVersion(Condition.ALLOW, Name.CREATE_MODEL_VERSION);
+    private static final CreateModelVersion DENY_INSTANCE =
+        new CreateModelVersion(Condition.DENY, Name.CREATE_MODEL_VERSION);
+
+    private CreateModelVersion(Condition condition, Name name) {
+      super(condition, name);
+    }
+
+    /** @return The instance with allow condition of the privilege. */
+    public static CreateModelVersion allow() {
+      return ALLOW_INSTANCE;
+    }
+
+    /** @return The instance with deny condition of the privilege. */
+    public static CreateModelVersion deny() {
+      return DENY_INSTANCE;
+    }
+
+    @Override
+    public boolean canBindTo(MetadataObject.Type type) {
+      return MODEL_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 5aa2fd8d2fa..e63e3d0982f 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/SecurableObjects.java
@@ -122,6 +122,22 @@ public static SecurableObject ofFileset(
     return of(MetadataObject.Type.FILESET, names, privileges);
   }
 
+  /**
+   * Create the model {@link SecurableObject} with the given securable schema object, model name and
+   * privileges.
+   *
+   * @param schema The schema securable object
+   * @param model The model name
+   * @param privileges The privileges of the fileset
+   * @return The created model {@link SecurableObject}
+   */
+  public static SecurableObject ofModel(
+      SecurableObject schema, String model, List<Privilege> privileges) {
+    List<String> names = Lists.newArrayList(DOT_SPLITTER.splitToList(schema.fullName()));
+    names.add(model);
+    return of(MetadataObject.Type.MODEL, names, privileges);
+  }
+
   private static class SecurableObjectImpl extends MetadataObjectImpl implements SecurableObject {
 
     private List<Privilege> privileges;
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 f3066666d9c..4283dd53cf2 100644
--- a/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
+++ b/api/src/test/java/org/apache/gravitino/authorization/TestSecurableObjects.java
@@ -177,6 +177,9 @@ public void testPrivileges() {
     Privilege manageUsers = Privileges.ManageUsers.allow();
     Privilege manageGroups = Privileges.ManageGroups.allow();
     Privilege manageGrants = Privileges.ManageGrants.allow();
+    Privilege createModel = Privileges.CreateModel.allow();
+    Privilege createModelVersion = Privileges.CreateModelVersion.allow();
+    Privilege useModel = Privileges.UseModel.allow();
 
     // Test create catalog
     Assertions.assertTrue(createCatalog.canBindTo(MetadataObject.Type.METALAKE));
@@ -347,5 +350,37 @@ public void testPrivileges() {
     Assertions.assertFalse(manageGrants.canBindTo(MetadataObject.Type.FILESET));
     Assertions.assertFalse(manageGrants.canBindTo(MetadataObject.Type.ROLE));
     Assertions.assertFalse(manageGrants.canBindTo(MetadataObject.Type.COLUMN));
+
+    // Test create model
+    Assertions.assertTrue(createModel.canBindTo(MetadataObject.Type.METALAKE));
+    Assertions.assertTrue(createModel.canBindTo(MetadataObject.Type.CATALOG));
+    Assertions.assertTrue(createModel.canBindTo(MetadataObject.Type.SCHEMA));
+    Assertions.assertFalse(createModel.canBindTo(MetadataObject.Type.TABLE));
+    Assertions.assertFalse(createModel.canBindTo(MetadataObject.Type.TOPIC));
+    Assertions.assertFalse(createModel.canBindTo(MetadataObject.Type.FILESET));
+    Assertions.assertFalse(createModel.canBindTo(MetadataObject.Type.ROLE));
+    Assertions.assertFalse(createModel.canBindTo(MetadataObject.Type.COLUMN));
+    Assertions.assertFalse(createModel.canBindTo(MetadataObject.Type.MODEL));
+    // Test create model version
+    Assertions.assertTrue(createModelVersion.canBindTo(MetadataObject.Type.METALAKE));
+    Assertions.assertTrue(createModelVersion.canBindTo(MetadataObject.Type.CATALOG));
+    Assertions.assertTrue(createModelVersion.canBindTo(MetadataObject.Type.SCHEMA));
+    Assertions.assertFalse(createModelVersion.canBindTo(MetadataObject.Type.TABLE));
+    Assertions.assertFalse(createModelVersion.canBindTo(MetadataObject.Type.TOPIC));
+    Assertions.assertFalse(createModelVersion.canBindTo(MetadataObject.Type.FILESET));
+    Assertions.assertFalse(createModelVersion.canBindTo(MetadataObject.Type.ROLE));
+    Assertions.assertFalse(createModelVersion.canBindTo(MetadataObject.Type.COLUMN));
+    Assertions.assertTrue(createModelVersion.canBindTo(MetadataObject.Type.MODEL));
+
+    // Test use model
+    Assertions.assertTrue(useModel.canBindTo(MetadataObject.Type.METALAKE));
+    Assertions.assertTrue(useModel.canBindTo(MetadataObject.Type.CATALOG));
+    Assertions.assertTrue(useModel.canBindTo(MetadataObject.Type.SCHEMA));
+    Assertions.assertFalse(useModel.canBindTo(MetadataObject.Type.TABLE));
+    Assertions.assertFalse(useModel.canBindTo(MetadataObject.Type.TOPIC));
+    Assertions.assertFalse(useModel.canBindTo(MetadataObject.Type.FILESET));
+    Assertions.assertFalse(useModel.canBindTo(MetadataObject.Type.ROLE));
+    Assertions.assertFalse(useModel.canBindTo(MetadataObject.Type.COLUMN));
+    Assertions.assertTrue(useModel.canBindTo(MetadataObject.Type.MODEL));
   }
 }
diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
index 1b24bb9083f..75262bb3c31 100644
--- a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
+++ b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
@@ -74,11 +74,20 @@ public void startIntegrationTest() throws Exception {
     Catalog filesetCatalog =
         metalake.createCatalog(
             "fileset_catalog", Catalog.Type.FILESET, "hadoop", "comment", Collections.emptyMap());
+
+    Catalog modelCatalog =
+        metalake.createCatalog(
+            "model_catalog", Catalog.Type.MODEL, "comment", Collections.emptyMap());
+
     NameIdentifier fileIdent = NameIdentifier.of("fileset_schema", "fileset");
     filesetCatalog.asSchemas().createSchema("fileset_schema", "comment", Collections.emptyMap());
     filesetCatalog
         .asFilesetCatalog()
         .createFileset(fileIdent, "comment", Fileset.Type.EXTERNAL, "tmp", Collections.emptyMap());
+
+    NameIdentifier modelIdent = NameIdentifier.of("model_schema", "model");
+    modelCatalog.asSchemas().createSchema("model_schema", "comment", Collections.emptyMap());
+    modelCatalog.asModelCatalog().registerModel(modelIdent, "comment", Collections.emptyMap());
   }
 
   @Test
@@ -218,6 +227,75 @@ void testManageRoles() {
     Assertions.assertEquals(createdPrivilege.name(), Privilege.Name.CREATE_CATALOG);
     Assertions.assertEquals(createdPrivilege.condition(), Privilege.Condition.ALLOW);
 
+    // Test a metalake object with model privileges
+    SecurableObject metalakeObjectWithModelPrivs =
+        SecurableObjects.ofMetalake(
+            metalakeName,
+            Lists.newArrayList(
+                Privileges.CreateModel.allow(),
+                Privileges.CreateModelVersion.allow(),
+                Privileges.UseModel.allow()));
+
+    role =
+        metalake.createRole(
+            "model_name", properties, Lists.newArrayList(metalakeObjectWithModelPrivs));
+
+    Assertions.assertEquals("model_name", role.name());
+    Assertions.assertEquals(properties, role.properties());
+    metalake.deleteRole("model_name");
+
+    SecurableObject catalogObjectWithModelPrivs =
+        SecurableObjects.ofCatalog(
+            "model_catalog",
+            Lists.newArrayList(
+                Privileges.CreateModel.allow(),
+                Privileges.CreateModelVersion.allow(),
+                Privileges.UseModel.allow()));
+    role =
+        metalake.createRole(
+            "model_name", properties, Lists.newArrayList(catalogObjectWithModelPrivs));
+    Assertions.assertEquals("model_name", role.name());
+    Assertions.assertEquals(properties, role.properties());
+    metalake.deleteRole("model_name");
+
+    SecurableObject schemaObjectWithModelPrivs =
+        SecurableObjects.ofSchema(
+            catalogObjectWithModelPrivs,
+            "model_schema",
+            Lists.newArrayList(
+                Privileges.CreateModel.allow(),
+                Privileges.CreateModelVersion.allow(),
+                Privileges.UseModel.allow()));
+    role =
+        metalake.createRole(
+            "model_name", properties, Lists.newArrayList(schemaObjectWithModelPrivs));
+    Assertions.assertEquals("model_name", role.name());
+    Assertions.assertEquals(properties, role.properties());
+    metalake.deleteRole("model_name");
+
+    SecurableObject modelObjectWithCorrectPriv =
+        SecurableObjects.ofModel(
+            schemaObjectWithModelPrivs,
+            "model",
+            Lists.newArrayList(Privileges.CreateModelVersion.allow(), Privileges.UseModel.allow()));
+    role =
+        metalake.createRole(
+            "model_name", properties, Lists.newArrayList(modelObjectWithCorrectPriv));
+    Assertions.assertEquals("model_name", role.name());
+    Assertions.assertEquals(properties, role.properties());
+    metalake.deleteRole("model_name");
+
+    SecurableObject modelObjectWithWrongPriv =
+        SecurableObjects.ofModel(
+            schemaObjectWithModelPrivs,
+            "model",
+            Lists.newArrayList(Privileges.CreateModel.allow()));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            metalake.createRole(
+                "model_name", properties, Lists.newArrayList(modelObjectWithWrongPriv)));
+
     // Test a not-existed metadata object
     SecurableObject catalogObject =
         SecurableObjects.ofCatalog(
@@ -257,6 +335,16 @@ void testManageRoles() {
         () ->
             metalake.createRole("not-existed", properties, Lists.newArrayList(wrongCatalogObject)));
 
+    // Create a role with wrong model privilege
+    SecurableObject wrongCatalogObject2 =
+        SecurableObjects.ofCatalog(
+            "fileset_catalog", Lists.newArrayList(Privileges.CreateModel.allow()));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            metalake.createRole(
+                "not-existed", properties, Lists.newArrayList(wrongCatalogObject2)));
+
     // Create a role with duplicated privilege
     SecurableObject duplicatedCatalogObject =
         SecurableObjects.ofCatalog(
diff --git a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
index 0400666bb7f..03f1ab58458 100644
--- a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
+++ b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
@@ -77,6 +77,12 @@ public class AuthorizationUtils {
       Sets.immutableEnumSet(
           Privilege.Name.CREATE_TOPIC, Privilege.Name.PRODUCE_TOPIC, Privilege.Name.CONSUME_TOPIC);
 
+  private static final Set<Privilege.Name> MODEL_PRIVILEGES =
+      Sets.immutableEnumSet(
+          Privilege.Name.CREATE_MODEL,
+          Privilege.Name.USE_MODEL,
+          Privilege.Name.CREATE_MODEL_VERSION);
+
   private AuthorizationUtils() {}
 
   public static void checkCurrentUser(String metalake, String user) {
@@ -245,6 +251,10 @@ public static void checkPrivilege(
         if (TOPIC_PRIVILEGES.contains(privilege.name())) {
           checkCatalogType(catalogIdent, Catalog.Type.MESSAGING, privilege);
         }
+
+        if (MODEL_PRIVILEGES.contains(privilege.name())) {
+          checkCatalogType(catalogIdent, Catalog.Type.MODEL, privilege);
+        }
       } catch (NoSuchCatalogException ne) {
         throw new NoSuchMetadataObjectException(
             "Securable object %s doesn't exist", object.fullName());
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index d5c676d92e1..f2319222f8c 100644
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -224,6 +224,14 @@ and `USE_SCHEMA` privileges on its parent schema.
 | WRITE_FILESET  | Metalake, Catalog, Schema, Fileset | Write a fileset (including alter a fileset) |
 | READ_FILESET   | Metalake, Catalog, Schema, Fileset | read a fileset                              |
 
+### Model privileges
+
+| Name                 | Supports Securable Object        | Operation                                                          |
+|----------------------|----------------------------------|--------------------------------------------------------------------|
+| CREATE_MODEL         | Metalake, Catalog, Schema        | Create a model                                                     |
+| CREATE_MODEL_VERSION | Metalake, Catalog, Schema, Model | Create a model version                                             |
+| USE_MODEL            | Metalake, Catalog, Schema, Model | View the metadata of the model and download all the model versions |
+
 ## Inheritance Model
 
 Securable objects in Gravitino are hierarchical and privileges are inherited downward.


Reviews:
user: login: xunliu
body: LGTM
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T10:03:29Z
user: login: jerryshao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T16:42:18Z
user: login: roryqi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T09:01:18Z
user: login: jerryshao
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:42:39Z

Review comments:
diff_hunk: @@ -218,6 +218,22 @@ void testManageRoles() {
     Assertions.assertEquals(createdPrivilege.name(), Privilege.Name.CREATE_CATALOG);
     Assertions.assertEquals(createdPrivilege.condition(), Privilege.Condition.ALLOW);
 
+    // Test a metalake object with model privileges
+    SecurableObject metalakeObjectWithModelPrivs =
+        SecurableObjects.ofMetalake(
+            metalakeName,
+            Lists.newArrayList(
+                Privileges.CreateModel.allow(),
+                Privileges.CreateModelVersion.allow(),
+                Privileges.UseModel.allow()));
+    role =
+        metalake.createRole(
+            "model_name", properties, Lists.newArrayList(metalakeObjectWithModelPrivs));
+
+    Assertions.assertEquals("model_name", role.name());
+    Assertions.assertEquals(properties, role.properties());
+    metalake.deleteRole("model_name");
path: clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
user: login: jerryshao
body: Can you please add more tests for model privileges on the catalog, schema and model? Also, please add some bad case verifications.
created_at: 2025-04-03T16:42:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,6 +218,22 @@ void testManageRoles() {
     Assertions.assertEquals(createdPrivilege.name(), Privilege.Name.CREATE_CATALOG);
     Assertions.assertEquals(createdPrivilege.condition(), Privilege.Condition.ALLOW);
 
+    // Test a metalake object with model privileges
+    SecurableObject metalakeObjectWithModelPrivs =
+        SecurableObjects.ofMetalake(
+            metalakeName,
+            Lists.newArrayList(
+                Privileges.CreateModel.allow(),
+                Privileges.CreateModelVersion.allow(),
+                Privileges.UseModel.allow()));
+    role =
+        metalake.createRole(
+            "model_name", properties, Lists.newArrayList(metalakeObjectWithModelPrivs));
+
+    Assertions.assertEquals("model_name", role.name());
+    Assertions.assertEquals(properties, role.properties());
+    metalake.deleteRole("model_name");
path: clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java
user: login: roryqi
body: OK.
created_at: 2025-04-07T09:01:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: jerryshao
created_at: 2025-04-02T17:46:44Z
body: Please also add the hook to the model operations.
author_association: CONTRIBUTOR
user: login: jerryshao
created_at: 2025-04-02T17:47:28Z
body: You can do this in another PR. https://github.com/apache/gravitino/issues/6704
author_association: CONTRIBUTOR
user: login: jerryshao
created_at: 2025-04-02T17:50:34Z
body: Do we need a privilege to support modifying model metadata?
author_association: CONTRIBUTOR
user: login: roryqi
created_at: 2025-04-03T02:08:41Z
body: > Do we need a privilege to support modifying model metadata?

In my view, the ownership have the ability to modify the model metadata align to other objects. If no other users give us similar input, we don't need to add the privilege now. 

> You can do this in another PR. #6704

OK.
author_association: CONTRIBUTOR
user: login: jerryshao
created_at: 2025-04-03T04:47:25Z
body: Can you add some tests to see if the model privileges can be set, stored, and fetched using current access control module?
author_association: CONTRIBUTOR
user: login: roryqi
created_at: 2025-04-03T06:09:55Z
body: > Can you add some tests to see if the model privileges can be set, stored, and fetched using current access control module?

OK.
author_association: CONTRIBUTOR
user: login: roryqi
created_at: 2025-04-03T08:03:36Z
body: > Can you add some tests to see if the model privileges can be set, stored, and fetched using current access control module?

Done.
author_association: CONTRIBUTOR