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

roryqi 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 9d3366b7db [#8814] feat(authz): Supports job API access control (#9283)
9d3366b7db is described below

commit 9d3366b7db239b84f9f10c2cd68f86b652307468
Author: roryqi <[email protected]>
AuthorDate: Tue Dec 2 14:12:17 2025 +0800

    [#8814] feat(authz): Supports job API access control (#9283)
    
    ### What changes were proposed in this pull request?
    
    Supports job API access control
    
    ### Why are the changes needed?
    
    Fix: #8814
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Added IT.
---
 .../test/authorization/JobAuthorizationIT.java     | 429 +++++++++++++++++++++
 .../java/org/apache/gravitino/GravitinoEnv.java    |  10 +-
 .../authorization/AuthorizationUtils.java          |  13 +-
 .../apache/gravitino/cache/ReverseIndexRules.java  |   4 +
 .../apache/gravitino/hook/JobHookDispatcher.java   | 132 +++++++
 .../org/apache/gravitino/policy/PolicyManager.java |  12 +-
 .../RelationalEntityStoreIdResolver.java           |  32 +-
 .../relational/mapper/JobTemplateMetaMapper.java   |   9 +
 .../mapper/JobTemplateMetaSQLProviderFactory.java  |   9 +
 .../base/JobTemplateMetaBaseSQLProvider.java       |  20 +
 .../relational/service/JobTemplateMetaService.java |  15 +
 .../relational/service/MetadataObjectService.java  |  67 +++-
 .../apache/gravitino/utils/MetadataObjectUtil.java |  33 +-
 .../apache/gravitino/utils/NameIdentifierUtil.java |  32 ++
 .../org/apache/gravitino/utils/NamespaceUtil.java  |  24 ++
 .../server/authorization/MetadataIdConverter.java  |   4 +
 .../annotations/AuthorizationRequest.java          |   3 +-
 .../AuthorizationExpressionConverter.java          |   5 +
 .../web/filter/GravitinoInterceptionService.java   |   4 +-
 .../gravitino/server/web/filter/ParameterUtil.java |  15 +
 .../authorization/AuthorizeExecutorFactory.java    |   7 +
 .../authorization/RunJobAuthorizationExecutor.java |  78 ++++
 .../gravitino/server/web/rest/JobOperations.java   |  85 +++-
 23 files changed, 984 insertions(+), 58 deletions(-)

diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/JobAuthorizationIT.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/JobAuthorizationIT.java
new file mode 100644
index 0000000000..befd664c2e
--- /dev/null
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/JobAuthorizationIT.java
@@ -0,0 +1,429 @@
+/*
+ * 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.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.authorization.Privileges;
+import org.apache.gravitino.client.GravitinoMetalake;
+import org.apache.gravitino.dto.MetalakeDTO;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.job.JobHandle;
+import org.apache.gravitino.job.JobTemplate;
+import org.apache.gravitino.job.ShellJobTemplate;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class JobAuthorizationIT extends BaseRestApiAuthorizationIT {
+
+  private static File testStagingDir;
+  private static ShellJobTemplate.Builder builder;
+  private static final String ROLE = "job_test_role";
+
+  @BeforeAll
+  public void startIntegrationTest() throws Exception {
+    testStagingDir = Files.createTempDirectory("test_staging_dir").toFile();
+    String testEntryScriptPath = generateTestEntryScript();
+    String testLibScriptPath = generateTestLibScript();
+
+    builder =
+        ShellJobTemplate.builder()
+            .withComment("Test shell job template")
+            .withExecutable(testEntryScriptPath)
+            .withArguments(Lists.newArrayList("{{arg1}}", "{{arg2}}"))
+            .withEnvironments(ImmutableMap.of("ENV_VAR", "{{env_var}}"))
+            .withScripts(Lists.newArrayList(testLibScriptPath))
+            .withCustomFields(Collections.emptyMap());
+
+    Map<String, String> configs =
+        ImmutableMap.of(
+            "gravitino.job.stagingDir",
+            testStagingDir.getAbsolutePath(),
+            "gravitino.job.statusPullIntervalInMs",
+            "3000");
+    registerCustomConfigs(configs);
+    super.startIntegrationTest();
+
+    // Create role for authorization tests
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    metalake.createRole(ROLE, new HashMap<>(), Collections.emptyList());
+    metalake.grantRolesToUser(ImmutableList.of(ROLE), NORMAL_USER);
+  }
+
+  @Test
+  @Order(1)
+  public void testRegisterJobTemplate() {
+    // User without privilege cannot register job template
+    assertThrows(
+        ForbiddenException.class,
+        () ->
+            normalUserClient
+                .loadMetalake(METALAKE)
+                .registerJobTemplate(builder.withName("test1").build()));
+
+    // Grant privilege to normal user
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(null, METALAKE, MetadataObject.Type.METALAKE),
+        ImmutableList.of(Privileges.RegisterJobTemplate.allow()));
+
+    // Now normal user can register job templates
+    JobTemplate template1 = builder.withName("test_1").build();
+    JobTemplate template2 = builder.withName("test_2").build();
+    normalUserClient.loadMetalake(METALAKE).registerJobTemplate(template1);
+    normalUserClient.loadMetalake(METALAKE).registerJobTemplate(template2);
+
+    // Admin can always register job templates
+    JobTemplate template3 = builder.withName("test_3").build();
+    metalake.registerJobTemplate(template3);
+  }
+
+  @Test
+  @Order(2)
+  public void testListJobTemplates() {
+    // Normal user can see job templates they own (test_1, test_2)
+    List<JobTemplate> normalUserTemplates =
+        normalUserClient.loadMetalake(METALAKE).listJobTemplates();
+    Assertions.assertEquals(2, normalUserTemplates.size());
+
+    // Admin can see all job templates (test_1, test_2, test_3)
+    List<JobTemplate> adminTemplates = 
client.loadMetalake(METALAKE).listJobTemplates();
+    Assertions.assertEquals(3, adminTemplates.size());
+  }
+
+  @Test
+  @Order(3)
+  public void testGetJobTemplate() {
+    // Normal user can get their own job template
+    JobTemplate template = 
normalUserClient.loadMetalake(METALAKE).getJobTemplate("test_1");
+    Assertions.assertNotNull(template);
+    Assertions.assertEquals("test_1", template.name());
+
+    // Normal user cannot get job template owned by admin
+    assertThrows(
+        ForbiddenException.class,
+        () -> 
normalUserClient.loadMetalake(METALAKE).getJobTemplate("test_3"));
+
+    // Admin can get any job template
+    JobTemplate adminTemplate = 
client.loadMetalake(METALAKE).getJobTemplate("test_3");
+    Assertions.assertNotNull(adminTemplate);
+  }
+
+  @Test
+  @Order(4)
+  public void testDeleteJobTemplate() {
+    // Normal user cannot delete job template owned by admin
+    assertThrows(
+        ForbiddenException.class,
+        () -> 
normalUserClient.loadMetalake(METALAKE).deleteJobTemplate("test_3"));
+
+    // Normal user can delete their own job template
+    boolean deleted = 
normalUserClient.loadMetalake(METALAKE).deleteJobTemplate("test_1");
+    Assertions.assertTrue(deleted);
+
+    // Admin can delete any job template
+    boolean adminDeleted = 
client.loadMetalake(METALAKE).deleteJobTemplate("test_3");
+    Assertions.assertTrue(adminDeleted);
+  }
+
+  @Test
+  @Order(5)
+  public void testRunJob() {
+    // Normal user without RunJob privilege cannot run jobs
+    assertThrows(
+        ForbiddenException.class,
+        () ->
+            normalUserClient
+                .loadMetalake(METALAKE)
+                .runJob(
+                    "test_2",
+                    ImmutableMap.of("arg1", "value1", "arg2", "success", 
"env_var", "value2")));
+
+    // Grant RunJob privilege to normal user but not UseJobTemplate privilege
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(null, METALAKE, MetadataObject.Type.METALAKE),
+        ImmutableList.of(Privileges.RunJob.allow()));
+
+    // User with RunJob privilege but without UseJobTemplate privilege cannot 
run jobs
+    // The authorization expression is: METALAKE::OWNER || (METALAKE::RUN_JOB 
&&
+    // (ANY_USE_JOB_TEMPLATE || JOB_TEMPLATE::OWNER))
+    
client.loadMetalake(METALAKE).registerJobTemplate(builder.withName("test_4").build());
+    assertThrows(
+        ForbiddenException.class,
+        () ->
+            normalUserClient
+                .loadMetalake(METALAKE)
+                .runJob(
+                    "test_4",
+                    ImmutableMap.of("arg1", "value1", "arg2", "success", 
"env_var", "value2")));
+
+    // Grant UseJobTemplate privilege on the metalake to normal user
+    metalake.grantPrivilegesToRole(
+        ROLE,
+        MetadataObjects.of(null, "test_2", MetadataObject.Type.JOB_TEMPLATE),
+        ImmutableList.of(Privileges.UseJobTemplate.allow()));
+    Assertions.assertDoesNotThrow(() -> metalake.getRole(ROLE));
+
+    // Now normal user can run jobs on their own template (has both RunJob and 
UseJobTemplate)
+    JobHandle normalUserJobHandle =
+        normalUserClient
+            .loadMetalake(METALAKE)
+            .runJob(
+                "test_2",
+                ImmutableMap.of("arg1", "value1", "arg2", "success", 
"env_var", "value2"));
+    Assertions.assertNotNull(normalUserJobHandle);
+    Assertions.assertEquals("test_2", normalUserJobHandle.jobTemplateName());
+
+    // Admin can run jobs on any template
+    JobHandle adminJobHandle =
+        metalake.runJob(
+            "test_2", ImmutableMap.of("arg1", "value3", "arg2", "success", 
"env_var", "value4"));
+    Assertions.assertNotNull(adminJobHandle);
+  }
+
+  @Test
+  @Order(6)
+  public void testListJob() {
+    // Normal user can see jobs they own (1 job from test_2 template)
+    List<JobHandle> normalUserJobs = 
normalUserClient.loadMetalake(METALAKE).listJobs();
+    Assertions.assertEquals(1, normalUserJobs.size());
+
+    // Admin can see all jobs (2 jobs total)
+    List<JobHandle> adminJobs = client.loadMetalake(METALAKE).listJobs();
+    Assertions.assertEquals(2, adminJobs.size());
+
+    // Listing jobs for specific template shows all jobs from that template
+    List<JobHandle> test2Jobs = 
normalUserClient.loadMetalake(METALAKE).listJobs("test_2");
+    Assertions.assertEquals(1, test2Jobs.size());
+  }
+
+  @Test
+  @Order(7)
+  public void testGetJob() {
+    // Get job IDs for testing
+    List<JobHandle> normalUserJobs = 
normalUserClient.loadMetalake(METALAKE).listJobs();
+    Assertions.assertTrue(normalUserJobs.size() > 0);
+    String normalUserJobId = normalUserJobs.get(0).jobId();
+
+    List<JobHandle> adminJobs = client.loadMetalake(METALAKE).listJobs();
+    String adminJobId = null;
+    for (JobHandle job : adminJobs) {
+      if (!job.jobId().equals(normalUserJobId)) {
+        adminJobId = job.jobId();
+        break;
+      }
+    }
+    Assertions.assertNotNull(adminJobId);
+
+    // Normal user can get their own job
+    JobHandle job = 
normalUserClient.loadMetalake(METALAKE).getJob(normalUserJobId);
+    Assertions.assertNotNull(job);
+    Assertions.assertEquals(normalUserJobId, job.jobId());
+
+    // Normal user cannot get job owned by admin
+    String finalAdminJobId = adminJobId;
+    assertThrows(
+        ForbiddenException.class,
+        () -> normalUserClient.loadMetalake(METALAKE).getJob(finalAdminJobId));
+
+    // Admin can get any job
+    JobHandle adminJob = client.loadMetalake(METALAKE).getJob(adminJobId);
+    Assertions.assertNotNull(adminJob);
+  }
+
+  @Test
+  @Order(8)
+  public void testCancelJob() {
+    // Get job IDs for testing
+    List<JobHandle> normalUserJobs = 
normalUserClient.loadMetalake(METALAKE).listJobs();
+    Assertions.assertTrue(normalUserJobs.size() > 0);
+    String normalUserJobId = normalUserJobs.get(0).jobId();
+
+    List<JobHandle> adminJobs = client.loadMetalake(METALAKE).listJobs();
+    String adminJobId = null;
+    for (JobHandle job : adminJobs) {
+      if (!job.jobId().equals(normalUserJobId)) {
+        adminJobId = job.jobId();
+        break;
+      }
+    }
+    Assertions.assertNotNull(adminJobId);
+
+    // Normal user cannot cancel job owned by admin
+    String finalAdminJobId = adminJobId;
+    assertThrows(
+        ForbiddenException.class,
+        () -> 
normalUserClient.loadMetalake(METALAKE).cancelJob(finalAdminJobId));
+
+    // Normal user can cancel their own job
+    JobHandle canceledJob = 
normalUserClient.loadMetalake(METALAKE).cancelJob(normalUserJobId);
+    Assertions.assertNotNull(canceledJob);
+
+    // Admin can cancel any job
+    JobHandle adminCanceledJob = 
client.loadMetalake(METALAKE).cancelJob(adminJobId);
+    Assertions.assertNotNull(adminCanceledJob);
+  }
+
+  @Test
+  @Order(9)
+  public void testJobOperationsWithNonExistentMetalake() throws Exception {
+    // Test that all job operations with @AuthorizationExpression return 403 
Forbidden
+    // when the metalake doesn't exist, instead of inconsistent 404 responses
+    String nonExistentMetalake = "nonExistentMetalake";
+
+    // Access the restClient from normalUserClient using reflection
+    Method restClientMethod =
+        
normalUserClient.getClass().getSuperclass().getDeclaredMethod("restClient");
+    restClientMethod.setAccessible(true);
+    Object restClient = restClientMethod.invoke(normalUserClient);
+
+    // Create a MetalakeDTO for the non-existent metalake
+    MetalakeDTO metalakeDTO =
+        MetalakeDTO.builder()
+            .withName(nonExistentMetalake)
+            .withComment("test")
+            .withProperties(Maps.newHashMap())
+            .withAudit(
+                org.apache.gravitino.dto.AuditDTO.builder()
+                    .withCreator("test")
+                    .withCreateTime(java.time.Instant.now())
+                    .build())
+            .build();
+
+    // Use DTOConverters.toMetaLake() via reflection to create 
GravitinoMetalake
+    Class<?> dtoConvertersClass = 
Class.forName("org.apache.gravitino.client.DTOConverters");
+    Method toMetaLakeMethod =
+        dtoConvertersClass.getDeclaredMethod(
+            "toMetaLake",
+            MetalakeDTO.class,
+            Class.forName("org.apache.gravitino.client.RESTClient"));
+    toMetaLakeMethod.setAccessible(true);
+    GravitinoMetalake nonExistentMetalakeObj =
+        (GravitinoMetalake) toMetaLakeMethod.invoke(null, metalakeDTO, 
restClient);
+
+    // Test registerJobTemplate - should return 403 ForbiddenException
+    assertThrows(
+        ForbiddenException.class,
+        () -> 
nonExistentMetalakeObj.registerJobTemplate(builder.withName("testTemplate").build()));
+
+    // Test listJobTemplates - should return 403 ForbiddenException
+    assertThrows(ForbiddenException.class, 
nonExistentMetalakeObj::listJobTemplates);
+
+    // Test getJobTemplate - should return 403 ForbiddenException
+    assertThrows(
+        ForbiddenException.class, () -> 
nonExistentMetalakeObj.getJobTemplate("testTemplate"));
+
+    // Test deleteJobTemplate - should return 403 ForbiddenException
+    assertThrows(
+        ForbiddenException.class, () -> 
nonExistentMetalakeObj.deleteJobTemplate("testTemplate"));
+
+    // Test runJob - should return 403 ForbiddenException
+    assertThrows(
+        ForbiddenException.class,
+        () -> nonExistentMetalakeObj.runJob("testTemplate", 
Maps.newHashMap()));
+
+    // Test listJobs - should return 403 ForbiddenException
+    assertThrows(ForbiddenException.class, nonExistentMetalakeObj::listJobs);
+
+    // Test listJobs with template name - should return 403 ForbiddenException
+    assertThrows(ForbiddenException.class, () -> 
nonExistentMetalakeObj.listJobs("testTemplate"));
+
+    // Test getJob - should return 403 ForbiddenException
+    assertThrows(ForbiddenException.class, () -> 
nonExistentMetalakeObj.getJob("testJobId"));
+
+    // Test cancelJob - should return 403 ForbiddenException
+    assertThrows(ForbiddenException.class, () -> 
nonExistentMetalakeObj.cancelJob("testJobId"));
+  }
+
+  @AfterAll
+  public static void tearDown() throws Exception {
+    if (testStagingDir != null && testStagingDir.exists()) {
+      FileUtils.deleteDirectory(testStagingDir);
+    }
+  }
+
+  private static String generateTestEntryScript() {
+    String content =
+        "#!/bin/bash\n"
+            + "echo \"starting test job\"\n\n"
+            + "bin=\"$(dirname \"${BASH_SOURCE-$0}\")\"\n"
+            + "bin=\"$(cd \"${bin}\">/dev/null; pwd)\"\n\n"
+            + ". \"${bin}/common.sh\"\n\n"
+            + "sleep 3\n\n"
+            + "JOB_NAME=\"test_job-$(date +%s)-$1\"\n\n"
+            + "echo \"Submitting job with name: $JOB_NAME\"\n\n"
+            + "echo \"$1\"\n\n"
+            + "echo \"$2\"\n\n"
+            + "echo \"$ENV_VAR\"\n\n"
+            + "if [[ \"$2\" == \"success\" ]]; then\n"
+            + "  exit 0\n"
+            + "elif [[ \"$2\" == \"fail\" ]]; then\n"
+            + "  exit 1\n"
+            + "else\n"
+            + "  exit 2\n"
+            + "fi\n";
+
+    try {
+      File scriptFile = new File(testStagingDir, "test-job.sh");
+      Files.writeString(scriptFile.toPath(), content);
+      if (!scriptFile.setExecutable(true)) {
+        throw new RuntimeException("Failed to set script as executable");
+      }
+      return scriptFile.getAbsolutePath();
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create test entry script", e);
+    }
+  }
+
+  private static String generateTestLibScript() {
+    String content = "#!/bin/bash\necho \"in common script\"\n";
+
+    try {
+      File scriptFile = new File(testStagingDir, "common.sh");
+      Files.writeString(scriptFile.toPath(), content);
+      if (!scriptFile.setExecutable(true)) {
+        throw new RuntimeException("Failed to set script as executable");
+      }
+      return scriptFile.getAbsolutePath();
+    } catch (Exception e) {
+      throw new RuntimeException("Failed to create test lib script", e);
+    }
+  }
+}
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java 
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index 318d07a624..34d5506420 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -53,6 +53,7 @@ import 
org.apache.gravitino.credential.CredentialOperationDispatcher;
 import org.apache.gravitino.hook.AccessControlHookDispatcher;
 import org.apache.gravitino.hook.CatalogHookDispatcher;
 import org.apache.gravitino.hook.FilesetHookDispatcher;
+import org.apache.gravitino.hook.JobHookDispatcher;
 import org.apache.gravitino.hook.MetalakeHookDispatcher;
 import org.apache.gravitino.hook.ModelHookDispatcher;
 import org.apache.gravitino.hook.PolicyHookDispatcher;
@@ -603,15 +604,16 @@ public class GravitinoEnv {
     this.auxServiceManager.serviceInit(config);
 
     // Create and initialize Tag related modules
-    TagEventDispatcher tagEventDispatcher =
-        new TagEventDispatcher(eventBus, new TagManager(idGenerator, 
entityStore));
-    this.tagDispatcher = new TagHookDispatcher(tagEventDispatcher);
+    TagManager tagManager = new TagManager(idGenerator, entityStore);
+    TagHookDispatcher tagHookDispatcher = new TagHookDispatcher(tagManager);
+    this.tagDispatcher = new TagEventDispatcher(eventBus, tagHookDispatcher);
 
     PolicyEventDispatcher policyEventDispatcher =
         new PolicyEventDispatcher(eventBus, new PolicyManager(idGenerator, 
entityStore));
     this.policyDispatcher = new PolicyHookDispatcher(policyEventDispatcher);
 
     this.jobOperationDispatcher =
-        new JobEventDispatcher(eventBus, new JobManager(config, entityStore, 
idGenerator));
+        new JobEventDispatcher(
+            eventBus, new JobHookDispatcher(new JobManager(config, 
entityStore, idGenerator)));
   }
 }
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 6fcd13121c..cd15d98904 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
@@ -66,6 +66,14 @@ public class AuthorizationUtils {
   static final String USER_DOES_NOT_EXIST_MSG = "User %s does not exist in the 
metalake %s";
   static final String GROUP_DOES_NOT_EXIST_MSG = "Group %s does not exist in 
the metalake %s";
   static final String ROLE_DOES_NOT_EXIST_MSG = "Role %s does not exist in the 
metalake %s";
+  private static final Set<MetadataObject.Type> SKIP_APPLY_TYPES =
+      Sets.newHashSet(
+          MetadataObject.Type.ROLE,
+          MetadataObject.Type.METALAKE,
+          MetadataObject.Type.JOB,
+          MetadataObject.Type.JOB_TEMPLATE,
+          MetadataObject.Type.TAG,
+          MetadataObject.Type.POLICY);
 
   private static final Set<Privilege.Name> FILESET_PRIVILEGES =
       Sets.immutableEnumSet(
@@ -357,10 +365,7 @@ public class AuthorizationUtils {
   }
 
   private static boolean needApplyAuthorization(MetadataObject.Type type) {
-    return type != MetadataObject.Type.ROLE
-        && type != MetadataObject.Type.METALAKE
-        && type != MetadataObject.Type.TAG
-        && type != MetadataObject.Type.POLICY;
+    return !SKIP_APPLY_TYPES.contains(type);
   }
 
   private static void callAuthorizationPluginImpl(
diff --git 
a/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java 
b/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java
index ace2fb044d..d457daa092 100644
--- a/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java
+++ b/core/src/main/java/org/apache/gravitino/cache/ReverseIndexRules.java
@@ -165,6 +165,10 @@ public class ReverseIndexRules {
                         entityType = Entity.EntityType.POLICY;
                         namespace = 
NamespaceUtil.ofPolicy(roleEntity.namespace().level(0));
                         break;
+                      case JOB_TEMPLATE:
+                        entityType = Entity.EntityType.JOB_TEMPLATE;
+                        namespace = 
NamespaceUtil.ofJobTemplate(roleEntity.namespace().level(0));
+                        break;
                       default:
                         throw new UnsupportedOperationException(
                             "Don't support securable object type: " + 
securableObject.type());
diff --git 
a/core/src/main/java/org/apache/gravitino/hook/JobHookDispatcher.java 
b/core/src/main/java/org/apache/gravitino/hook/JobHookDispatcher.java
new file mode 100644
index 0000000000..a6dac9bbd9
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/hook/JobHookDispatcher.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.hook;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.Owner;
+import org.apache.gravitino.authorization.OwnerDispatcher;
+import org.apache.gravitino.exceptions.InUseException;
+import org.apache.gravitino.exceptions.JobTemplateAlreadyExistsException;
+import org.apache.gravitino.exceptions.NoSuchJobException;
+import org.apache.gravitino.exceptions.NoSuchJobTemplateException;
+import org.apache.gravitino.job.JobOperationDispatcher;
+import org.apache.gravitino.job.JobTemplateChange;
+import org.apache.gravitino.meta.JobEntity;
+import org.apache.gravitino.meta.JobTemplateEntity;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.PrincipalUtils;
+
+public class JobHookDispatcher implements JobOperationDispatcher {
+  private final JobOperationDispatcher jobOperationDispatcher;
+
+  public JobHookDispatcher(JobOperationDispatcher jobOperationDispatcher) {
+    this.jobOperationDispatcher = jobOperationDispatcher;
+  }
+
+  @Override
+  public List<JobTemplateEntity> listJobTemplates(String metalake) {
+    return jobOperationDispatcher.listJobTemplates(metalake);
+  }
+
+  @Override
+  public void registerJobTemplate(String metalake, JobTemplateEntity 
jobTemplateEntity)
+      throws JobTemplateAlreadyExistsException {
+    // Check whether the current user exists or not
+    AuthorizationUtils.checkCurrentUser(metalake, 
PrincipalUtils.getCurrentUserName());
+
+    jobOperationDispatcher.registerJobTemplate(metalake, jobTemplateEntity);
+
+    // Set the creator as the owner of the job template.
+    OwnerDispatcher ownerManager = 
GravitinoEnv.getInstance().ownerDispatcher();
+    if (ownerManager != null) {
+      ownerManager.setOwner(
+          metalake,
+          NameIdentifierUtil.toMetadataObject(
+              jobTemplateEntity.nameIdentifier(), 
Entity.EntityType.JOB_TEMPLATE),
+          PrincipalUtils.getCurrentUserName(),
+          Owner.Type.USER);
+    }
+  }
+
+  @Override
+  public JobTemplateEntity getJobTemplate(String metalake, String 
jobTemplateName)
+      throws NoSuchJobTemplateException {
+    return jobOperationDispatcher.getJobTemplate(metalake, jobTemplateName);
+  }
+
+  @Override
+  public boolean deleteJobTemplate(String metalake, String jobTemplateName) 
throws InUseException {
+    return jobOperationDispatcher.deleteJobTemplate(metalake, jobTemplateName);
+  }
+
+  @Override
+  public JobTemplateEntity alterJobTemplate(
+      String metalake, String jobTemplateName, JobTemplateChange... changes)
+      throws NoSuchJobTemplateException, IllegalArgumentException {
+    return jobOperationDispatcher.alterJobTemplate(metalake, jobTemplateName, 
changes);
+  }
+
+  @Override
+  public List<JobEntity> listJobs(String metalake, Optional<String> 
jobTemplateName)
+      throws NoSuchJobTemplateException {
+    return jobOperationDispatcher.listJobs(metalake, jobTemplateName);
+  }
+
+  @Override
+  public JobEntity getJob(String metalake, String jobId) throws 
NoSuchJobException {
+    return jobOperationDispatcher.getJob(metalake, jobId);
+  }
+
+  @Override
+  public JobEntity runJob(String metalake, String jobTemplateName, Map<String, 
String> jobConf)
+      throws NoSuchJobTemplateException {
+    // Check whether the current user exists or not
+    AuthorizationUtils.checkCurrentUser(metalake, 
PrincipalUtils.getCurrentUserName());
+
+    JobEntity jobEntity = jobOperationDispatcher.runJob(metalake, 
jobTemplateName, jobConf);
+
+    // Set the creator as the owner of the job.
+    OwnerDispatcher ownerManager = 
GravitinoEnv.getInstance().ownerDispatcher();
+    if (ownerManager != null) {
+      ownerManager.setOwner(
+          metalake,
+          NameIdentifierUtil.toMetadataObject(jobEntity.nameIdentifier(), 
Entity.EntityType.JOB),
+          PrincipalUtils.getCurrentUserName(),
+          Owner.Type.USER);
+    }
+
+    return jobEntity;
+  }
+
+  @Override
+  public JobEntity cancelJob(String metalake, String jobId) throws 
NoSuchJobException {
+    return jobOperationDispatcher.cancelJob(metalake, jobId);
+  }
+
+  @Override
+  public void close() throws IOException {
+    jobOperationDispatcher.close();
+  }
+}
diff --git a/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java 
b/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
index fe39649b9d..bcb8bb0439 100644
--- a/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
+++ b/core/src/main/java/org/apache/gravitino/policy/PolicyManager.java
@@ -56,6 +56,14 @@ import org.slf4j.LoggerFactory;
 public class PolicyManager implements PolicyDispatcher {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(PolicyManager.class);
+  private static final Set<MetadataObject.Type> 
SUPPORTED_METADATA_OBJECT_TYPES_FOR_POLICIES =
+      Sets.newHashSet(
+          MetadataObject.Type.CATALOG,
+          MetadataObject.Type.SCHEMA,
+          MetadataObject.Type.TABLE,
+          MetadataObject.Type.FILESET,
+          MetadataObject.Type.TOPIC,
+          MetadataObject.Type.MODEL);
 
   private final IdGenerator idGenerator;
   private final EntityStore entityStore;
@@ -295,9 +303,7 @@ public class PolicyManager implements PolicyDispatcher {
       String[] policiesToAdd,
       String[] policiesToRemove) {
     Preconditions.checkArgument(
-        !metadataObject.type().equals(MetadataObject.Type.METALAKE)
-            && !metadataObject.type().equals(MetadataObject.Type.ROLE)
-            && !metadataObject.type().equals(MetadataObject.Type.COLUMN),
+        
SUPPORTED_METADATA_OBJECT_TYPES_FOR_POLICIES.contains(metadataObject.type()),
         "Cannot associate policies for unsupported metadata object type %s",
         metadataObject.type());
 
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStoreIdResolver.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStoreIdResolver.java
index 5794eeb358..1f32e25658 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStoreIdResolver.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStoreIdResolver.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableSet;
 import java.util.Set;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.job.JobHandle;
 import org.apache.gravitino.meta.EntityIdResolver;
 import org.apache.gravitino.meta.NamespacedEntityId;
 import org.apache.gravitino.storage.relational.helper.CatalogIds;
@@ -29,6 +30,7 @@ import 
org.apache.gravitino.storage.relational.helper.SchemaIds;
 import org.apache.gravitino.storage.relational.service.CatalogMetaService;
 import org.apache.gravitino.storage.relational.service.FilesetMetaService;
 import org.apache.gravitino.storage.relational.service.GroupMetaService;
+import org.apache.gravitino.storage.relational.service.JobTemplateMetaService;
 import org.apache.gravitino.storage.relational.service.MetalakeMetaService;
 import org.apache.gravitino.storage.relational.service.ModelMetaService;
 import org.apache.gravitino.storage.relational.service.PolicyMetaService;
@@ -49,10 +51,14 @@ public class RelationalEntityStoreIdResolver implements 
EntityIdResolver {
           Entity.EntityType.USER,
           Entity.EntityType.GROUP,
           Entity.EntityType.TAG,
-          Entity.EntityType.POLICY);
-  private static final Set<Entity.EntityType> 
ENTITY_TYPES_REQURING_CATALOG_IDS =
+          Entity.EntityType.POLICY,
+          Entity.EntityType.JOB,
+          Entity.EntityType.JOB_TEMPLATE);
+
+  private static final Set<Entity.EntityType> 
ENTITY_TYPES_REQUIRING_CATALOG_IDS =
       ImmutableSet.of(Entity.EntityType.CATALOG);
-  private static final Set<Entity.EntityType> ENTITY_TYPES_REQURING_SCHEMA_IDS 
=
+
+  private static final Set<Entity.EntityType> 
ENTITY_TYPES_REQUIRING_SCHEMA_IDS =
       ImmutableSet.of(
           Entity.EntityType.SCHEMA,
           Entity.EntityType.TABLE,
@@ -66,7 +72,7 @@ public class RelationalEntityStoreIdResolver implements 
EntityIdResolver {
     if (ENTITY_TYPES_REQUIRING_METALAKE_ID.contains(type)) {
       return getEntityIdsRequiringMetalakeId(nameIdentifier, type);
 
-    } else if (ENTITY_TYPES_REQURING_CATALOG_IDS.contains(type)) {
+    } else if (ENTITY_TYPES_REQUIRING_CATALOG_IDS.contains(type)) {
       CatalogIds catalogIds =
           CatalogMetaService.getInstance()
               .getCatalogIdByMetalakeAndCatalogName(
@@ -74,7 +80,7 @@ public class RelationalEntityStoreIdResolver implements 
EntityIdResolver {
                   
NameIdentifierUtil.getCatalogIdentifier(nameIdentifier).name());
       return new NamespacedEntityId(catalogIds.getCatalogId(), 
catalogIds.getMetalakeId());
 
-    } else if (ENTITY_TYPES_REQURING_SCHEMA_IDS.contains(type)) {
+    } else if (ENTITY_TYPES_REQUIRING_SCHEMA_IDS.contains(type)) {
       return getEntityIdsRequiringSchemaIds(nameIdentifier, type);
 
     } else {
@@ -87,14 +93,14 @@ public class RelationalEntityStoreIdResolver implements 
EntityIdResolver {
     if (ENTITY_TYPES_REQUIRING_METALAKE_ID.contains(type)) {
       return getEntityIdsRequiringMetalakeId(nameIdentifier, type).entityId();
 
-    } else if (ENTITY_TYPES_REQURING_CATALOG_IDS.contains(type)) {
+    } else if (ENTITY_TYPES_REQUIRING_CATALOG_IDS.contains(type)) {
       return CatalogMetaService.getInstance()
           .getCatalogIdByMetalakeAndCatalogName(
               NameIdentifierUtil.getMetalake(nameIdentifier),
               NameIdentifierUtil.getCatalogIdentifier(nameIdentifier).name())
           .getCatalogId();
 
-    } else if (ENTITY_TYPES_REQURING_SCHEMA_IDS.contains(type)) {
+    } else if (ENTITY_TYPES_REQUIRING_SCHEMA_IDS.contains(type)) {
       return getEntityIdsRequiringSchemaIds(nameIdentifier, type).entityId();
 
     } else {
@@ -140,6 +146,18 @@ public class RelationalEntityStoreIdResolver implements 
EntityIdResolver {
             PolicyMetaService.getInstance()
                 .getPolicyIdByPolicyName(metalakeId, nameIdentifier.name());
         return new NamespacedEntityId(policyId, metalakeId);
+
+      case JOB_TEMPLATE:
+        long jobTemplateId =
+            JobTemplateMetaService.getInstance()
+                .getJobTemplateIdByMetalakeIdAndName(metalakeId, 
nameIdentifier.name());
+        return new NamespacedEntityId(jobTemplateId, metalakeId);
+
+      case JOB:
+        long jobId =
+            
Long.parseLong(nameIdentifier.name().substring(JobHandle.JOB_ID_PREFIX.length()));
+        return new NamespacedEntityId(jobId, metalakeId);
+
       default:
         throw new IllegalArgumentException("Unsupported entity type: " + type);
     }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
index 19af2cc44a..1ca511e755 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaMapper.java
@@ -70,4 +70,13 @@ public interface JobTemplateMetaMapper {
   Integer updateJobTemplateMeta(
       @Param("newJobTemplateMeta") JobTemplatePO newJobTemplatePO,
       @Param("oldJobTemplateMeta") JobTemplatePO oldJobTemplatePO);
+
+  @SelectProvider(
+      type = JobTemplateMetaSQLProviderFactory.class,
+      method = "selectJobTemplateIdByMetalakeAndName")
+  Long selectJobTemplateIdByMetalakeAndName(
+      @Param("metalakeId") long metalakeId, @Param("jobTemplateName") String 
jobTemplateName);
+
+  @SelectProvider(type = JobTemplateMetaSQLProviderFactory.class, method = 
"selectJobTemplateById")
+  JobTemplatePO selectJobTemplateById(Long jobTemplateId);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
index 4dc6908e0d..a6b04e5947 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/JobTemplateMetaSQLProviderFactory.java
@@ -93,4 +93,13 @@ public class JobTemplateMetaSQLProviderFactory {
       @Param("oldJobTemplateMeta") JobTemplatePO oldJobTemplatePO) {
     return getProvider().updateJobTemplateMeta(newJobTemplatePO, 
oldJobTemplatePO);
   }
+
+  public static String selectJobTemplateIdByMetalakeAndName(
+      @Param("metalakeId") Long metalakeId, @Param("jobTemplateName") String 
jobTemplateName) {
+    return getProvider().selectJobTemplateIdByMetalakeAndName(metalakeId, 
jobTemplateName);
+  }
+
+  public static String selectJobTemplateById(@Param("jobTemplateId") Long 
jobTemplateId) {
+    return getProvider().selectJobTemplateById(jobTemplateId);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
index 3f6a1082b5..3c2c2e82c9 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/JobTemplateMetaBaseSQLProvider.java
@@ -141,4 +141,24 @@ public class JobTemplateMetaBaseSQLProvider {
         + " AND last_version = #{oldJobTemplateMeta.lastVersion}"
         + " AND deleted_at = 0";
   }
+
+  public String selectJobTemplateIdByMetalakeAndName(
+      @Param("metalakeId") Long metalakeId, @Param("jobTemplateName") String 
jobTemplateName) {
+    return "SELECT job_template_id FROM "
+        + JobTemplateMetaMapper.TABLE_NAME
+        + " WHERE deleted_at = 0 AND metalake_id = #{metalakeId} AND 
job_template_name = #{jobTemplateName}";
+  }
+
+  public String selectJobTemplateById(@Param("jobTemplateId") Long 
jobTemplateId) {
+    return "SELECT jtm.job_template_id AS jobTemplateId, jtm.job_template_name 
AS jobTemplateName,"
+        + " jtm.metalake_id AS metalakeId, jtm.job_template_comment AS 
jobTemplateComment,"
+        + " jtm.job_template_content AS jobTemplateContent, jtm.audit_info AS 
auditInfo,"
+        + " jtm.current_version AS currentVersion, jtm.last_version AS 
lastVersion,"
+        + " jtm.deleted_at AS deletedAt"
+        + " FROM "
+        + JobTemplateMetaMapper.TABLE_NAME
+        + " jtm"
+        + " WHERE jtm.job_template_id = #{jobTemplateId}"
+        + " AND jtm.deleted_at = 0";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
index ad38114a70..cfaf51f2d6 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/JobTemplateMetaService.java
@@ -204,4 +204,19 @@ public class JobTemplateMetaService {
     }
     return jobTemplatePO;
   }
+
+  public long getJobTemplateIdByMetalakeIdAndName(long metalakeId, String 
name) {
+    Long jobTemplateId =
+        SessionUtils.getWithoutCommit(
+            JobTemplateMetaMapper.class,
+            mapper -> mapper.selectJobTemplateIdByMetalakeAndName(metalakeId, 
name));
+
+    if (jobTemplateId == null) {
+      throw new NoSuchEntityException(
+          NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+          Entity.EntityType.JOB_TEMPLATE.name().toLowerCase(Locale.ROOT),
+          name);
+    }
+    return jobTemplateId;
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/MetadataObjectService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/MetadataObjectService.java
index 75682d843f..e4b9425704 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/MetadataObjectService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/MetadataObjectService.java
@@ -23,6 +23,7 @@ import static 
org.apache.gravitino.metrics.source.MetricsSource.GRAVITINO_RELATI
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -31,10 +32,12 @@ import java.util.stream.Collectors;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.job.JobHandle;
 import org.apache.gravitino.meta.GenericEntity;
 import org.apache.gravitino.metrics.Monitored;
 import org.apache.gravitino.storage.relational.mapper.CatalogMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.FilesetMetaMapper;
+import org.apache.gravitino.storage.relational.mapper.JobTemplateMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.ModelMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.PolicyMetaMapper;
@@ -67,27 +70,22 @@ public class MetadataObjectService {
 
   static final Map<MetadataObject.Type, Function<List<Long>, Map<Long, 
String>>>
       TYPE_TO_FULLNAME_FUNCTION_MAP =
-          ImmutableMap.of(
-              MetadataObject.Type.METALAKE,
-              MetadataObjectService::getMetalakeObjectsFullName,
-              MetadataObject.Type.CATALOG,
-              MetadataObjectService::getCatalogObjectsFullName,
-              MetadataObject.Type.SCHEMA,
-              MetadataObjectService::getSchemaObjectsFullName,
-              MetadataObject.Type.TABLE,
-              MetadataObjectService::getTableObjectsFullName,
-              MetadataObject.Type.FILESET,
-              MetadataObjectService::getFilesetObjectsFullName,
-              MetadataObject.Type.MODEL,
-              MetadataObjectService::getModelObjectsFullName,
-              MetadataObject.Type.TOPIC,
-              MetadataObjectService::getTopicObjectsFullName,
-              MetadataObject.Type.COLUMN,
-              MetadataObjectService::getColumnObjectsFullName,
-              MetadataObject.Type.TAG,
-              MetadataObjectService::getTagObjectsFullName,
-              MetadataObject.Type.POLICY,
-              MetadataObjectService::getPolicyObjectsFullName);
+          ImmutableMap.<MetadataObject.Type, Function<List<Long>, Map<Long, 
String>>>builder()
+              .put(MetadataObject.Type.METALAKE, 
MetadataObjectService::getMetalakeObjectsFullName)
+              .put(MetadataObject.Type.CATALOG, 
MetadataObjectService::getCatalogObjectsFullName)
+              .put(MetadataObject.Type.SCHEMA, 
MetadataObjectService::getSchemaObjectsFullName)
+              .put(MetadataObject.Type.TABLE, 
MetadataObjectService::getTableObjectsFullName)
+              .put(MetadataObject.Type.FILESET, 
MetadataObjectService::getFilesetObjectsFullName)
+              .put(MetadataObject.Type.MODEL, 
MetadataObjectService::getModelObjectsFullName)
+              .put(MetadataObject.Type.TOPIC, 
MetadataObjectService::getTopicObjectsFullName)
+              .put(MetadataObject.Type.COLUMN, 
MetadataObjectService::getColumnObjectsFullName)
+              .put(MetadataObject.Type.TAG, 
MetadataObjectService::getTagObjectsFullName)
+              .put(MetadataObject.Type.POLICY, 
MetadataObjectService::getPolicyObjectsFullName)
+              .put(MetadataObject.Type.JOB, 
MetadataObjectService::getJobObjectsFullName)
+              .put(
+                  MetadataObject.Type.JOB_TEMPLATE,
+                  MetadataObjectService::getJobTemplateObjectsFullName)
+              .build();
 
   private static Map<Long, String> getPolicyObjectsFullName(List<Long> 
policyIds) {
     if (policyIds == null || policyIds.isEmpty()) {
@@ -104,6 +102,33 @@ public class MetadataObjectService {
                             
policyMetaMapper.selectPolicyByPolicyId(policyId).getPolicyName())));
   }
 
+  private static Map<Long, String> getJobObjectsFullName(List<Long> jobIds) {
+    if (jobIds == null || jobIds.isEmpty()) {
+      return Maps.newHashMap();
+    }
+
+    return jobIds.stream()
+        .collect(Collectors.toMap(jobId -> jobId, jobId -> 
JobHandle.JOB_ID_PREFIX + jobId));
+  }
+
+  private static Map<Long, String> getJobTemplateObjectsFullName(List<Long> 
jobTemplateIds) {
+    if (jobTemplateIds == null || jobTemplateIds.isEmpty()) {
+      return Maps.newHashMap();
+    }
+
+    return jobTemplateIds.stream()
+        .collect(
+            Collectors.toMap(
+                jobTemplateId -> jobTemplateId,
+                jobTemplateId ->
+                    SessionUtils.getWithoutCommit(
+                        JobTemplateMetaMapper.class,
+                        jobTemplateMetaMapper ->
+                            jobTemplateMetaMapper
+                                .selectJobTemplateById(jobTemplateId)
+                                .jobTemplateName())));
+  }
+
   private static Map<Long, String> getTagObjectsFullName(List<Long> tagIds) {
     if (tagIds == null || tagIds.isEmpty()) {
       return Map.of();
diff --git 
a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
index c33a0786a5..09f7ed271c 100644
--- a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
@@ -33,6 +33,8 @@ import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AuthorizationUtils;
 import org.apache.gravitino.exceptions.IllegalMetadataObjectException;
+import org.apache.gravitino.exceptions.NoSuchJobException;
+import org.apache.gravitino.exceptions.NoSuchJobTemplateException;
 import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
 import org.apache.gravitino.exceptions.NoSuchPolicyException;
 import org.apache.gravitino.exceptions.NoSuchRoleException;
@@ -55,6 +57,8 @@ public class MetadataObjectUtil {
           .put(MetadataObject.Type.MODEL, Entity.EntityType.MODEL)
           .put(MetadataObject.Type.TAG, Entity.EntityType.TAG)
           .put(MetadataObject.Type.POLICY, Entity.EntityType.POLICY)
+          .put(MetadataObject.Type.JOB_TEMPLATE, 
Entity.EntityType.JOB_TEMPLATE)
+          .put(MetadataObject.Type.JOB, Entity.EntityType.JOB)
           .build();
 
   private MetadataObjectUtil() {}
@@ -112,6 +116,10 @@ public class MetadataObjectUtil {
         return NameIdentifierUtil.ofTag(metalakeName, metadataObject.name());
       case POLICY:
         return NameIdentifierUtil.ofPolicy(metalakeName, 
metadataObject.name());
+      case JOB:
+        return NameIdentifierUtil.ofJob(metalakeName, metadataObject.name());
+      case JOB_TEMPLATE:
+        return NameIdentifierUtil.ofJobTemplate(metalakeName, 
metadataObject.name());
       case CATALOG:
       case SCHEMA:
       case TABLE:
@@ -196,21 +204,19 @@ public class MetadataObjectUtil {
         try {
           env.accessControlDispatcher().getRole(metalake, object.fullName());
         } catch (NoSuchRoleException nsr) {
-          Preconditions.checkArgument(
-              exceptionToThrowSupplier != null, "exceptionToThrowSupplier 
should not be null");
           throw exceptionToThrowSupplier.get();
         }
         break;
+
       case TAG:
         NameIdentifierUtil.checkTag(identifier);
         try {
           env.tagDispatcher().getTag(metalake, object.fullName());
         } catch (NoSuchTagException nsr) {
-          Preconditions.checkArgument(
-              exceptionToThrowSupplier != null, "exceptionToThrowSupplier 
should not be null");
           throw exceptionToThrowSupplier.get();
         }
         break;
+
       case POLICY:
         NameIdentifierUtil.checkPolicy(identifier);
         try {
@@ -219,6 +225,25 @@ public class MetadataObjectUtil {
           throw checkNotNull(exceptionToThrowSupplier).get();
         }
         break;
+
+      case JOB:
+        NameIdentifierUtil.checkJob(identifier);
+        try {
+          env.jobOperationDispatcher().getJob(metalake, object.fullName());
+        } catch (NoSuchJobException e) {
+          throw exceptionToThrowSupplier.get();
+        }
+        break;
+
+      case JOB_TEMPLATE:
+        NameIdentifierUtil.checkJobTemplate(identifier);
+        try {
+          env.jobOperationDispatcher().getJobTemplate(metalake, 
object.fullName());
+        } catch (NoSuchJobTemplateException e) {
+          throw exceptionToThrowSupplier.get();
+        }
+        break;
+
       default:
         throw new IllegalArgumentException(
             String.format("Doesn't support the type %s", object.type()));
diff --git 
a/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
index 73b23868c3..b607e3bfd5 100644
--- a/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
@@ -517,6 +517,28 @@ public class NameIdentifierUtil {
     NamespaceUtil.checkModelVersion(ident.namespace());
   }
 
+  /**
+   * Check the given {@link NameIdentifier} is a job identifier. Throw an 
{@link
+   * IllegalNameIdentifierException} if it's not.
+   *
+   * @param ident The job {@link NameIdentifier} to check.
+   */
+  public static void checkJob(NameIdentifier ident) {
+    NameIdentifier.check(ident != null, "Job identifier must not be null");
+    NamespaceUtil.checkJob(ident.namespace());
+  }
+
+  /**
+   * Check the given {@link NameIdentifier} is a job template identifier. 
Throw an {@link
+   * IllegalNameIdentifierException} if it's not.
+   *
+   * @param ident The job template {@link NameIdentifier} to check.
+   */
+  public static void checkJobTemplate(NameIdentifier ident) {
+    NameIdentifier.check(ident != null, "Job template identifier must not be 
null");
+    NamespaceUtil.checkJobTemplate(ident.namespace());
+  }
+
   /**
    * Convert the given {@link NameIdentifier} and {@link Entity.EntityType} to 
{@link
    * MetadataObject}.
@@ -578,9 +600,19 @@ public class NameIdentifierUtil {
       case TAG:
         checkTag(ident);
         return MetadataObjects.of(null, ident.name(), MetadataObject.Type.TAG);
+
       case POLICY:
         checkPolicy(ident);
         return MetadataObjects.of(null, ident.name(), 
MetadataObject.Type.POLICY);
+
+      case JOB:
+        checkJob(ident);
+        return MetadataObjects.of(null, ident.name(), MetadataObject.Type.JOB);
+
+      case JOB_TEMPLATE:
+        checkJobTemplate(ident);
+        return MetadataObjects.of(null, ident.name(), 
MetadataObject.Type.JOB_TEMPLATE);
+
       default:
         throw new IllegalArgumentException(
             "Entity type " + entityType + " is not supported to convert to 
MetadataObject");
diff --git a/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java 
b/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java
index c294a90145..1578c264fa 100644
--- a/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/NamespaceUtil.java
@@ -337,6 +337,30 @@ public class NamespaceUtil {
         namespace);
   }
 
+  /**
+   * Check if the given job template namespace is legal, throw an {@link 
IllegalNamespaceException}
+   *
+   * @param namespace The job template namespace
+   */
+  public static void checkJobTemplate(Namespace namespace) {
+    check(
+        namespace != null && namespace.length() == 3,
+        "Job template namespace must be non-null and have 3 levels, the input 
namespace is %s",
+        namespace);
+  }
+
+  /**
+   * Check if the given job namespace is legal, throw an {@link 
IllegalNamespaceException}
+   *
+   * @param namespace The job namespace
+   */
+  public static void checkJob(Namespace namespace) {
+    check(
+        namespace != null && namespace.length() == 3,
+        "Job namespace must be non-null and have 3 levels, the input namespace 
is %s",
+        namespace);
+  }
+
   /**
    * Check the given condition is true. Throw an {@link 
IllegalNamespaceException} if it's not.
    *
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataIdConverter.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataIdConverter.java
index c563c45373..6f674d291a 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataIdConverter.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataIdConverter.java
@@ -38,6 +38,8 @@ import org.apache.gravitino.meta.CatalogEntity;
 import org.apache.gravitino.meta.ColumnEntity;
 import org.apache.gravitino.meta.FilesetEntity;
 import org.apache.gravitino.meta.GroupEntity;
+import org.apache.gravitino.meta.JobEntity;
+import org.apache.gravitino.meta.JobTemplateEntity;
 import org.apache.gravitino.meta.ModelEntity;
 import org.apache.gravitino.meta.ModelVersionEntity;
 import org.apache.gravitino.meta.RoleEntity;
@@ -76,6 +78,8 @@ public class MetadataIdConverter {
           .put(Entity.EntityType.USER, UserEntity.class)
           .put(Entity.EntityType.GROUP, GroupEntity.class)
           .put(Entity.EntityType.ROLE, RoleEntity.class)
+          .put(Entity.EntityType.JOB_TEMPLATE, JobTemplateEntity.class)
+          .put(Entity.EntityType.JOB, JobEntity.class)
           .build();
 
   private MetadataIdConverter() {}
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
index ee75d4f08b..325cecfe08 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
@@ -31,6 +31,7 @@ public @interface AuthorizationRequest {
   enum RequestType {
     COMMON,
     ASSOCIATE_TAG,
-    ASSOCIATE_POLICY
+    ASSOCIATE_POLICY,
+    RUN_JOB
   }
 }
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 424e910f35..d4901eb1ba 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
@@ -283,6 +283,11 @@ public class AuthorizationExpressionConverter {
             "ANY_APPLY_POLICY",
             "((ANY(APPLY_POLICY, METALAKE, POLICY))"
                 + "&& !(ANY(DENY_APPLY_POLICY, METALAKE, POLICY)))");
+    expression =
+        expression.replaceAll(
+            "ANY_USE_JOB_TEMPLATE",
+            "((ANY(USE_JOB_TEMPLATE, METALAKE, JOB_TEMPLATE))"
+                + "&& !(ANY(DENY_USE_JOB_TEMPLATE, METALAKE, JOB_TEMPLATE)))");
     expression =
         expression.replaceAll(
             CAN_SET_OWNER,
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index 9d463c99a9..cefdba0b9a 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -51,6 +51,7 @@ import 
org.apache.gravitino.server.web.filter.authorization.AuthorizeExecutorFac
 import org.apache.gravitino.server.web.rest.CatalogOperations;
 import org.apache.gravitino.server.web.rest.FilesetOperations;
 import org.apache.gravitino.server.web.rest.GroupOperations;
+import org.apache.gravitino.server.web.rest.JobOperations;
 import org.apache.gravitino.server.web.rest.MetadataObjectPolicyOperations;
 import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
 import org.apache.gravitino.server.web.rest.MetalakeOperations;
@@ -101,7 +102,8 @@ public class GravitinoInterceptionService implements 
InterceptionService {
             MetadataObjectTagOperations.class.getName(),
             TagOperations.class.getName(),
             PolicyOperations.class.getName(),
-            MetadataObjectPolicyOperations.class.getName()));
+            MetadataObjectPolicyOperations.class.getName(),
+            JobOperations.class.getName()));
   }
 
   @Override
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
index 068a9af7e5..ddefaf01fb 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
@@ -159,11 +159,26 @@ public class ParameterUtil {
                   Entity.EntityType.TAG,
                   NameIdentifierUtil.ofTag(metalake, 
entities.get(Entity.EntityType.TAG)));
               break;
+
             case POLICY:
               nameIdentifierMap.put(
                   Entity.EntityType.POLICY,
                   NameIdentifierUtil.ofPolicy(metalake, 
entities.get(Entity.EntityType.POLICY)));
               break;
+
+            case JOB:
+              nameIdentifierMap.put(
+                  Entity.EntityType.JOB,
+                  NameIdentifierUtil.ofJob(metalake, 
entities.get(Entity.EntityType.JOB)));
+              break;
+
+            case JOB_TEMPLATE:
+              nameIdentifierMap.put(
+                  Entity.EntityType.JOB_TEMPLATE,
+                  NameIdentifierUtil.ofJobTemplate(
+                      metalake, entities.get(Entity.EntityType.JOB_TEMPLATE)));
+              break;
+
             default:
               break;
           }
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
index 2fbe7a69db..b6051b1c88 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
@@ -54,6 +54,13 @@ public class AuthorizeExecutorFactory {
           authorizationExpressionEvaluator,
           pathParams,
           entityType);
+      case RUN_JOB -> new RunJobAuthorizationExecutor(
+          parameters,
+          args,
+          metadataContext,
+          authorizationExpressionEvaluator,
+          pathParams,
+          entityType);
     };
   }
 }
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
new file mode 100644
index 0000000000..a4c5ea2039
--- /dev/null
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.web.filter.authorization;
+
+import static 
org.apache.gravitino.server.web.filter.ParameterUtil.extractFromParameters;
+
+import com.google.common.base.Preconditions;
+import java.lang.reflect.Parameter;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.dto.requests.JobRunRequest;
+import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+public class RunJobAuthorizationExecutor implements AuthorizationExecutor {
+  private final Parameter[] parameters;
+  private final Object[] args;
+  private final Map<Entity.EntityType, NameIdentifier> metadataContext;
+  private final AuthorizationExpressionEvaluator 
authorizationExpressionEvaluator;
+  private final Map<String, Object> pathParams;
+  private final String entityType;
+
+  public RunJobAuthorizationExecutor(
+      Parameter[] parameters,
+      Object[] args,
+      Map<Entity.EntityType, NameIdentifier> metadataContext,
+      AuthorizationExpressionEvaluator authorizationExpressionEvaluator,
+      Map<String, Object> pathParams,
+      String entityType) {
+    this.parameters = parameters;
+    this.args = args;
+    this.metadataContext = metadataContext;
+    this.authorizationExpressionEvaluator = authorizationExpressionEvaluator;
+    this.pathParams = pathParams;
+    this.entityType = entityType;
+  }
+
+  @Override
+  public boolean execute() throws Exception {
+    Object request = extractFromParameters(parameters, args);
+    if (request == null) {
+      return false;
+    }
+
+    AuthorizationRequestContext context = new AuthorizationRequestContext();
+    Preconditions.checkArgument(
+        request instanceof JobRunRequest,
+        "Expected JobRunRequest but found %s",
+        request.getClass().getSimpleName());
+    JobRunRequest jobRunRequest = (JobRunRequest) request;
+    jobRunRequest.validate();
+    String jobTemplateName = jobRunRequest.getJobTemplateName();
+    NameIdentifier metalake = metadataContext.get(Entity.EntityType.METALAKE);
+    NameIdentifier jobTemplateNameIdentifier =
+        NameIdentifierUtil.ofJobTemplate(metalake.name(), jobTemplateName);
+    metadataContext.put(Entity.EntityType.JOB_TEMPLATE, 
jobTemplateNameIdentifier);
+
+    return authorizationExpressionEvaluator.evaluate(
+        metadataContext, pathParams, context, Optional.ofNullable(entityType));
+  }
+}
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
index ca5bc551e6..c29b14f9f3 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.server.web.rest;
 import com.codahale.metrics.annotation.ResponseMetered;
 import com.codahale.metrics.annotation.Timed;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
@@ -40,6 +41,7 @@ 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.GravitinoEnv;
 import org.apache.gravitino.dto.job.JobDTO;
 import org.apache.gravitino.dto.job.JobTemplateDTO;
@@ -63,7 +65,12 @@ import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.JobEntity;
 import org.apache.gravitino.meta.JobTemplateEntity;
 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.annotations.AuthorizationRequest;
 import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.apache.gravitino.utils.NamespaceUtil;
 import org.apache.gravitino.utils.PrincipalUtils;
 import org.slf4j.Logger;
@@ -88,8 +95,10 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "list-job-templates." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "list-job-templates", absolute = true)
+  @AuthorizationExpression(expression = "")
   public Response listJobTemplates(
-      @PathParam("metalake") String metalake,
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
       @QueryParam("details") @DefaultValue("false") boolean details) {
     LOG.info(
         "Received request to list job templates in metalake: {}, details: {}", 
metalake, details);
@@ -98,8 +107,19 @@ public class JobOperations {
       return Utils.doAs(
           httpRequest,
           () -> {
-            List<JobTemplateDTO> jobTemplates =
-                
toJobTemplateDTOs(jobOperationDispatcher.listJobTemplates(metalake));
+            List<JobTemplateEntity> jobTemplateEntities =
+                Lists.newArrayList(
+                    MetadataAuthzHelper.filterByExpression(
+                        metalake,
+                        "METALAKE::OWNER || JOB_TEMPLATE::OWNER || 
ANY_USE_JOB_TEMPLATE",
+                        Entity.EntityType.JOB_TEMPLATE,
+                        jobOperationDispatcher
+                            .listJobTemplates(metalake)
+                            .toArray(new JobTemplateEntity[0]),
+                        jobTemplateEntity ->
+                            NameIdentifierUtil.ofJobTemplate(metalake, 
jobTemplateEntity.name())));
+            List<JobTemplateDTO> jobTemplates = 
toJobTemplateDTOs(jobTemplateEntities);
+
             if (details) {
               LOG.info("List {} job templates in metalake: {}", 
jobTemplates.size(), metalake);
               return Utils.ok(new JobTemplateListResponse(jobTemplates));
@@ -124,8 +144,11 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "register-job-template." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "register-job-template", absolute = true)
+  @AuthorizationExpression(expression = "METALAKE::OWNER || 
METALAKE::REGISTER_JOB_TEMPLATE")
   public Response registerJobTemplate(
-      @PathParam("metalake") String metalake, JobTemplateRegisterRequest 
request) {
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      JobTemplateRegisterRequest request) {
     LOG.info(
         "Received request to register job template {} in metalake: {}",
         request.getJobTemplate().name(),
@@ -158,8 +181,13 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "get-job-template." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "get-job-template", absolute = true)
+  @AuthorizationExpression(
+      expression = "METALAKE::OWNER || JOB_TEMPLATE::OWNER || 
ANY_USE_JOB_TEMPLATE")
   public Response getJobTemplate(
-      @PathParam("metalake") String metalake, @PathParam("name") String name) {
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @PathParam("name") @AuthorizationMetadata(type = 
Entity.EntityType.JOB_TEMPLATE)
+          String name) {
     LOG.info("Received request to get job template: {} in metalake: {}", name, 
metalake);
 
     try {
@@ -183,8 +211,12 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "delete-job-template." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "delete-job-template", absolute = true)
+  @AuthorizationExpression(expression = "METALAKE::OWNER || 
JOB_TEMPLATE::OWNER")
   public Response deleteJobTemplate(
-      @PathParam("metalake") String metalake, @PathParam("name") String name) {
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @PathParam("name") @AuthorizationMetadata(type = 
Entity.EntityType.JOB_TEMPLATE)
+          String name) {
     LOG.info("Received request to delete job template: {} in metalake: {}", 
name, metalake);
 
     try {
@@ -211,9 +243,12 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "alter-job-template." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
   @ResponseMetered(name = "alter-job-template", absolute = true)
+  @AuthorizationExpression(expression = "METALAKE::OWNER || 
JOB_TEMPLATE::OWNER")
   public Response alterJobTemplate(
-      @PathParam("metalake") String metalake,
-      @PathParam("name") String jobTemplateName,
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @PathParam("name") @AuthorizationMetadata(type = 
Entity.EntityType.JOB_TEMPLATE)
+          String jobTemplateName,
       JobTemplateUpdatesRequest request) {
     LOG.info(
         "Received request to alter job template: {} in metalake: {}", 
jobTemplateName, metalake);
@@ -245,8 +280,10 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "list-jobs." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "list-jobs", absolute = true)
+  @AuthorizationExpression(expression = "")
   public Response listJobs(
-      @PathParam("metalake") String metalake,
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
       @QueryParam("jobTemplateName") String jobTemplateName) {
     LOG.info(
         "Received request to list jobs in metalake {}{}",
@@ -258,7 +295,15 @@ public class JobOperations {
           httpRequest,
           () -> {
             List<JobEntity> jobEntities =
-                jobOperationDispatcher.listJobs(metalake, 
Optional.ofNullable(jobTemplateName));
+                Lists.newArrayList(
+                    MetadataAuthzHelper.filterByExpression(
+                        metalake,
+                        "METALAKE::OWNER || JOB::OWNER",
+                        Entity.EntityType.JOB,
+                        jobOperationDispatcher
+                            .listJobs(metalake, 
Optional.ofNullable(jobTemplateName))
+                            .toArray(new JobEntity[0]),
+                        jobEntity -> NameIdentifierUtil.ofJob(metalake, 
jobEntity.name())));
             List<JobDTO> jobDTOs = toJobDTOs(jobEntities);
 
             LOG.info("Listed {} jobs in metalake {}", jobEntities.size(), 
metalake);
@@ -275,7 +320,11 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "get-job." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "get-job", absolute = true)
-  public Response getJob(@PathParam("metalake") String metalake, 
@PathParam("jobId") String jobId) {
+  @AuthorizationExpression(expression = "METALAKE::OWNER || JOB::OWNER")
+  public Response getJob(
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @PathParam("jobId") @AuthorizationMetadata(type = Entity.EntityType.JOB) 
String jobId) {
     LOG.info("Received request to get job {} in metalake {}", jobId, metalake);
 
     try {
@@ -297,7 +346,14 @@ public class JobOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "run-job." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "run-job", absolute = true)
-  public Response runJob(@PathParam("metalake") String metalake, JobRunRequest 
request) {
+  @AuthorizationExpression(
+      expression =
+          "METALAKE::OWNER || (METALAKE::RUN_JOB && (ANY_USE_JOB_TEMPLATE || 
JOB_TEMPLATE::OWNER))")
+  public Response runJob(
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @AuthorizationRequest(type = AuthorizationRequest.RequestType.RUN_JOB)
+          JobRunRequest request) {
     LOG.info(
         "Received request to run job {} in metalake: {}", 
request.getJobTemplateName(), metalake);
 
@@ -329,8 +385,11 @@ public class JobOperations {
   @Path("runs/{jobId}")
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "cancel-job." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @AuthorizationExpression(expression = "METALAKE::OWNER || JOB::OWNER")
   public Response cancelJob(
-      @PathParam("metalake") String metalake, @PathParam("jobId") String 
jobId) {
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @PathParam("jobId") @AuthorizationMetadata(type = Entity.EntityType.JOB) 
String jobId) {
     LOG.info("Received request to cancel job {} in metalake {}", jobId, 
metalake);
 
     try {

Reply via email to