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

jinsongzhou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/amoro.git


The following commit(s) were added to refs/heads/master by this push:
     new 6c037912f [AMORO-4117] Implement dashboard RBAC with jCasbin and LDAP 
role mapping (#4118)
6c037912f is described below

commit 6c037912f7ada649f69afb643041a75eeeea8825
Author: Xu Bai <[email protected]>
AuthorDate: Thu Mar 26 16:49:58 2026 +0800

    [AMORO-4117] Implement dashboard RBAC with jCasbin and LDAP role mapping 
(#4118)
    
    * [Feature] Implement role-based access control with ADMIN and READ_ONLY 
roles
    
    * Ensure username and password are converted to String in authentication 
and role resolution
    
    * Update RBAC documentation and configuration examples in ams-config.md and 
ConfigurationsTest.java
    
    * [Feature] Update role-based access control to use SERVICE_ADMIN and 
VIEWER roles
    
    * [Feature] Add RBAC configuration for SERVICE_ADMIN and VIEWER roles
    
    * [Feature] Update RBAC model and policy files with licensing information 
and new dependencies
    
    * [Feature] Refactor RBAC implementation to use string-based roles and 
enhance privilege mapping
    
    * Update RBAC default role handling and documentation
    
    * Update RBAC documentation to reflect changes in default role handling and 
introduce new roles
    
    * Fix comment in LdapPasswdAuthenticationProvider to reflect correct email 
example for username normalization
    
    * revert
---
 .gitignore                                         |   2 +
 amoro-ams/pom.xml                                  |  12 ++
 .../apache/amoro/server/AmoroManagementConf.java   |  54 +++++
 .../authentication/HttpAuthenticationFactory.java  |   9 +-
 .../LdapPasswdAuthenticationProvider.java          |  17 +-
 .../server/authorization/AuthorizationRequest.java |  54 +++++
 .../authorization/CasbinAuthorizationManager.java  | 148 +++++++++++++
 .../authorization/DashboardPrivilegeMapper.java    | 159 ++++++++++++++
 .../DashboardPrivilegeMappingRule.java             |  50 +++++
 .../authorization/LdapGroupRoleResolver.java       | 239 +++++++++++++++++++++
 .../amoro/server/authorization/Privilege.java      |  31 +++
 .../amoro/server/authorization/ResourceType.java   |  28 +++
 .../apache/amoro/server/authorization/Role.java    |  27 +++
 .../amoro/server/authorization/RoleResolver.java   |  87 ++++++++
 .../amoro/server/dashboard/DashboardServer.java    |  41 +++-
 .../dashboard/controller/LoginController.java      |  74 +++++--
 .../resources/authorization/amoro_rbac_model.conf  |  28 +++
 .../resources/authorization/amoro_rbac_policy.csv  |  32 +++
 .../resources/authorization/privilege_mapping.yaml |  97 +++++++++
 .../apache/amoro/config/ConfigurationsTest.java    | 109 +++++++++-
 .../CasbinAuthorizationManagerTest.java            |  47 ++++
 .../authorization/LdapGroupRoleResolverTest.java   | 158 ++++++++++++++
 .../server/authorization/RoleResolverTest.java     | 113 ++++++++++
 amoro-web/mock/modules/common.js                   |  11 +-
 amoro-web/src/components/Sidebar.vue               |  46 +++-
 amoro-web/src/components/Topbar.vue                |  15 +-
 amoro-web/src/main.ts                              |  66 ++++++
 amoro-web/src/router/index.ts                      |   2 +-
 amoro-web/src/store/index.ts                       |   2 +
 amoro-web/src/types/common.type.ts                 |   3 +
 amoro-web/src/utils/permission.ts                  | 106 +++++++++
 amoro-web/src/utils/request.ts                     |  28 ++-
 amoro-web/src/views/catalogs/Detail.vue            |   6 +-
 amoro-web/src/views/catalogs/index.vue             |   6 +-
 amoro-web/src/views/hive-details/index.vue         |   5 +-
 amoro-web/src/views/login/index.vue                |  11 +
 amoro-web/src/views/resource/components/List.vue   |   6 +-
 amoro-web/src/views/resource/index.vue             |   7 +-
 .../src/views/tables/components/Optimizing.vue     |   4 +-
 amoro-web/src/views/terminal/index.vue             |  13 +-
 dev/deps/dependencies-hadoop-2-spark-3.3           |   8 +-
 dev/deps/dependencies-hadoop-3-spark-3.5           |   8 +-
 dist/src/main/amoro-bin/conf/config.yaml           |  32 ++-
 docs/configuration/ams-config.md                   |  93 ++++++++
 pom.xml                                            |   1 +
 45 files changed, 2041 insertions(+), 54 deletions(-)

diff --git a/.gitignore b/.gitignore
index c140957d0..58055849d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,8 @@ amoro-web/node/
 public
 resources
 !javadoc/resources
+!amoro-ams/src/main/resources/
+!amoro-ams/src/main/resources/**
 
 !dist/src/main/amoro-bin/bin/
 !dist/src/main/amoro-bin/conf/
diff --git a/amoro-ams/pom.xml b/amoro-ams/pom.xml
index 3f83f5e02..34f5591c7 100644
--- a/amoro-ams/pom.xml
+++ b/amoro-ams/pom.xml
@@ -218,6 +218,18 @@
             <artifactId>mybatis</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.casbin</groupId>
+            <artifactId>jcasbin</artifactId>
+            <version>${jcasbin.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.20.0</version>
+        </dependency>
+
         <dependency>
             <groupId>org.postgresql</groupId>
             <artifactId>postgresql</artifactId>
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
index fd0e70cf2..3a31933bf 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
@@ -26,6 +26,7 @@ import org.apache.amoro.utils.MemorySize;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 public class AmoroManagementConf {
 
@@ -53,6 +54,59 @@ public class AmoroManagementConf {
           .defaultValue("admin")
           .withDescription("The administrator password");
 
+  public static final ConfigOption<Boolean> AUTHORIZATION_ENABLED =
+      ConfigOptions.key("http-server.authorization.enabled")
+          .booleanType()
+          .defaultValue(false)
+          .withDescription("Whether to enable dashboard RBAC authorization.");
+
+  public static final ConfigOption<String> AUTHORIZATION_DEFAULT_ROLE =
+      ConfigOptions.key("http-server.authorization.default-role")
+          .stringType()
+          .noDefaultValue()
+          .withDescription(
+              "Optional default dashboard role for authenticated users without 
an LDAP role mapping.");
+
+  public static final ConfigOption<Boolean> 
AUTHORIZATION_LDAP_ROLE_MAPPING_ENABLED =
+      ConfigOptions.key("http-server.authorization.ldap-role-mapping.enabled")
+          .booleanType()
+          .defaultValue(false)
+          .withDescription("Whether to resolve dashboard roles from LDAP group 
membership.");
+
+  public static final ConfigOption<String> 
AUTHORIZATION_LDAP_ROLE_MAPPING_GROUP_MEMBER_ATTRIBUTE =
+      
ConfigOptions.key("http-server.authorization.ldap-role-mapping.group-member-attribute")
+          .stringType()
+          .defaultValue("member")
+          .withDescription("LDAP group attribute that stores member 
references.");
+
+  public static final ConfigOption<String> 
AUTHORIZATION_LDAP_ROLE_MAPPING_USER_DN_PATTERN =
+      
ConfigOptions.key("http-server.authorization.ldap-role-mapping.user-dn-pattern")
+          .stringType()
+          .noDefaultValue()
+          .withDescription(
+              "LDAP user DN pattern used to match group members. Use {0} as 
the username placeholder.");
+
+  public static final ConfigOption<List<Map<String, String>>>
+      AUTHORIZATION_LDAP_ROLE_MAPPING_GROUPS =
+          
ConfigOptions.key("http-server.authorization.ldap-role-mapping.groups")
+              .mapType()
+              .asList()
+              .noDefaultValue()
+              .withDescription(
+                  "LDAP group-to-role mapping entries containing group-dn and 
role fields.");
+
+  public static final ConfigOption<String> 
AUTHORIZATION_LDAP_ROLE_MAPPING_BIND_DN =
+      ConfigOptions.key("http-server.authorization.ldap-role-mapping.bind-dn")
+          .stringType()
+          .defaultValue("")
+          .withDescription("Optional LDAP bind DN used when querying 
role-mapping groups.");
+
+  public static final ConfigOption<String> 
AUTHORIZATION_LDAP_ROLE_MAPPING_BIND_PASSWORD =
+      
ConfigOptions.key("http-server.authorization.ldap-role-mapping.bind-password")
+          .stringType()
+          .defaultValue("")
+          .withDescription("Optional LDAP bind password used when querying 
role-mapping groups.");
+
   /** Enable master & slave mode, which supports horizontal scaling of AMS. */
   public static final ConfigOption<Boolean> USE_MASTER_SLAVE_MODE =
       ConfigOptions.key("use-master-slave-mode")
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authentication/HttpAuthenticationFactory.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authentication/HttpAuthenticationFactory.java
index 2699741c5..7a4ba1450 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/authentication/HttpAuthenticationFactory.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authentication/HttpAuthenticationFactory.java
@@ -56,8 +56,15 @@ public class HttpAuthenticationFactory {
           .impl(className)
           .<T>buildChecked()
           .newInstance(conf);
+    } catch (NoSuchMethodException e) {
+      throw new IllegalStateException(
+          className
+              + " must implement "
+              + expected.getName()
+              + " and provide a public constructor (Configurations) or no-arg 
constructor",
+          e);
     } catch (Exception e) {
-      throw new IllegalStateException(className + " must extend of " + 
expected.getName());
+      throw new IllegalStateException("Failed to create " + className + ": " + 
e.getMessage(), e);
     }
   }
 
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProvider.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProvider.java
index 4ce70d793..f95df8bc7 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProvider.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authentication/LdapPasswdAuthenticationProvider.java
@@ -54,12 +54,14 @@ public class LdapPasswdAuthenticationProvider implements 
PasswdAuthenticationPro
 
   @Override
   public BasicPrincipal authenticate(PasswordCredential credential) throws 
SignatureCheckException {
+    String username = normalizeUsername(credential.username());
     Hashtable<String, Object> env = new Hashtable<String, Object>();
     env.put(Context.INITIAL_CONTEXT_FACTORY, 
"com.sun.jndi.ldap.LdapCtxFactory");
     env.put(Context.PROVIDER_URL, ldapUrl);
     env.put(Context.SECURITY_AUTHENTICATION, "simple");
     env.put(Context.SECURITY_CREDENTIALS, credential.password());
-    env.put(Context.SECURITY_PRINCIPAL, formatter.format(new String[] 
{credential.username()}));
+    env.put(Context.SECURITY_PRINCIPAL, formatter.format(new String[] 
{username}));
+    env.put(Context.REFERRAL, "follow");
 
     InitialDirContext initialLdapContext = null;
     try {
@@ -75,6 +77,17 @@ public class LdapPasswdAuthenticationProvider implements 
PasswdAuthenticationPro
         }
       }
     }
-    return new BasicPrincipal(credential.username());
+    return new BasicPrincipal(username);
+  }
+
+  /**
+   * Strip email domain suffix if present so that "[email protected]" becomes 
"amoro". The LDAP
+   * user-pattern template expects a plain username, not an email address.
+   */
+  private static String normalizeUsername(String username) {
+    if (username != null && username.contains("@")) {
+      return username.substring(0, username.indexOf('@'));
+    }
+    return username;
   }
 }
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/AuthorizationRequest.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/AuthorizationRequest.java
new file mode 100644
index 000000000..bcb16a353
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/AuthorizationRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.amoro.server.authorization;
+
+public class AuthorizationRequest {
+  public static final String GLOBAL_RESOURCE_ID = "GLOBAL";
+
+  private final ResourceType resourceType;
+  private final String resourceId;
+  private final Privilege privilege;
+
+  private AuthorizationRequest(ResourceType resourceType, String resourceId, 
Privilege privilege) {
+    this.resourceType = resourceType;
+    this.resourceId = resourceId;
+    this.privilege = privilege;
+  }
+
+  public static AuthorizationRequest of(ResourceType resourceType, Privilege 
privilege) {
+    return new AuthorizationRequest(resourceType, GLOBAL_RESOURCE_ID, 
privilege);
+  }
+
+  public static AuthorizationRequest of(
+      ResourceType resourceType, String resourceId, Privilege privilege) {
+    return new AuthorizationRequest(resourceType, resourceId, privilege);
+  }
+
+  public ResourceType getResourceType() {
+    return resourceType;
+  }
+
+  public String getResourceId() {
+    return resourceId;
+  }
+
+  public Privilege getPrivilege() {
+    return privilege;
+  }
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/CasbinAuthorizationManager.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/CasbinAuthorizationManager.java
new file mode 100644
index 000000000..70f01ee02
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/CasbinAuthorizationManager.java
@@ -0,0 +1,148 @@
+/*
+ * 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.amoro.server.authorization;
+
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.casbin.jcasbin.main.Enforcer;
+import org.casbin.jcasbin.model.Model;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class CasbinAuthorizationManager {
+  private static final String MODEL_RESOURCE = 
"authorization/amoro_rbac_model.conf";
+  private static final String POLICY_RESOURCE = 
"authorization/amoro_rbac_policy.csv";
+
+  private final boolean authorizationEnabled;
+  private final Enforcer enforcer;
+  private final Map<String, Set<String>> rolePrivileges;
+
+  public CasbinAuthorizationManager(Configurations serviceConfig) {
+    this.authorizationEnabled = 
serviceConfig.get(AmoroManagementConf.AUTHORIZATION_ENABLED);
+    this.enforcer = authorizationEnabled ? createEnforcer() : null;
+    this.rolePrivileges =
+        authorizationEnabled ? loadRolePrivileges(POLICY_RESOURCE) : 
Collections.emptyMap();
+  }
+
+  public boolean isAuthorizationEnabled() {
+    return authorizationEnabled;
+  }
+
+  public boolean authorize(Set<String> roles, AuthorizationRequest request) {
+    if (!authorizationEnabled) {
+      return true;
+    }
+
+    if (roles == null || roles.isEmpty()) {
+      return false;
+    }
+
+    for (String role : roles) {
+      if (Boolean.TRUE.equals(
+          enforcer.enforce(
+              role,
+              request.getResourceType().name(),
+              request.getResourceId(),
+              request.getPrivilege().name()))) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public Set<String> resolvePrivileges(Set<String> roles) {
+    if (!authorizationEnabled) {
+      return java.util.Arrays.stream(Privilege.values())
+          .map(Enum::name)
+          .collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    if (roles == null || roles.isEmpty()) {
+      return Collections.emptySet();
+    }
+
+    LinkedHashSet<String> privileges = new LinkedHashSet<>();
+    for (String role : roles) {
+      privileges.addAll(rolePrivileges.getOrDefault(role, 
Collections.emptySet()));
+    }
+    return Collections.unmodifiableSet(privileges);
+  }
+
+  private static Enforcer createEnforcer() {
+    Model model = new Model();
+    model.loadModelFromText(readResource(MODEL_RESOURCE));
+
+    Enforcer enforcer = new Enforcer(model);
+    for (List<String> policy : readPolicies(POLICY_RESOURCE)) {
+      if (!policy.isEmpty() && "p".equals(policy.get(0))) {
+        enforcer.addPolicy(policy.subList(1, policy.size()));
+      }
+    }
+    return enforcer;
+  }
+
+  private static List<List<String>> readPolicies(String resource) {
+    return readResource(resource)
+        .lines()
+        .map(String::trim)
+        .filter(line -> !line.isEmpty() && !line.startsWith("#"))
+        .map(
+            line ->
+                List.of(line.split("\\s*,\\s*")).stream()
+                    .map(String::trim)
+                    .collect(Collectors.toList()))
+        .collect(Collectors.toList());
+  }
+
+  private static Map<String, Set<String>> loadRolePrivileges(String resource) {
+    return readPolicies(resource).stream()
+        .filter(policy -> policy.size() >= 5 && "p".equals(policy.get(0)))
+        .filter(policy -> policy.size() < 6 || 
!"deny".equalsIgnoreCase(policy.get(5)))
+        .collect(
+            Collectors.groupingBy(
+                policy -> policy.get(1),
+                Collectors.mapping(
+                    policy -> policy.get(4), 
Collectors.toCollection(LinkedHashSet::new))));
+  }
+
+  private static String readResource(String resource) {
+    try (InputStream inputStream =
+            
CasbinAuthorizationManager.class.getClassLoader().getResourceAsStream(resource);
+        InputStreamReader inputStreamReader =
+            new InputStreamReader(
+                Objects.requireNonNull(inputStream, "Missing resource: " + 
resource),
+                StandardCharsets.UTF_8);
+        BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) 
{
+      return 
bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()));
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to load authorization resource: " + 
resource, e);
+    }
+  }
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/DashboardPrivilegeMapper.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/DashboardPrivilegeMapper.java
new file mode 100644
index 000000000..dbe752ec5
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/DashboardPrivilegeMapper.java
@@ -0,0 +1,159 @@
+/*
+ * 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.amoro.server.authorization;
+
+import io.javalin.http.Context;
+import org.apache.amoro.shade.guava32.com.google.common.base.Preconditions;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class DashboardPrivilegeMapper {
+  private static final String MAPPING_RESOURCE = 
"authorization/privilege_mapping.yaml";
+
+  private final List<DashboardPrivilegeMappingRule> mappingRules;
+
+  public DashboardPrivilegeMapper() {
+    this.mappingRules = loadRules();
+  }
+
+  public Optional<AuthorizationRequest> resolve(Context ctx) {
+    String method = ctx.method().toUpperCase();
+    String path = ctx.path();
+    return mappingRules.stream()
+        .filter(rule -> rule.matches(method, path))
+        .map(DashboardPrivilegeMappingRule::getAuthorizationRequest)
+        .findFirst();
+  }
+
+  private static List<DashboardPrivilegeMappingRule> loadRules() {
+    Map<String, Object> root = loadYaml(MAPPING_RESOURCE);
+    Object mappingsObject = root.get("mappings");
+    if (!(mappingsObject instanceof List)) {
+      throw new IllegalArgumentException(
+          "Invalid dashboard privilege mapping resource: missing mappings 
list");
+    }
+
+    List<?> mappings = (List<?>) mappingsObject;
+    return mappings.stream()
+        .map(item -> toRule(castMap(item, "mapping entry")))
+        .collect(Collectors.toList());
+  }
+
+  private static DashboardPrivilegeMappingRule toRule(Map<String, Object> 
ruleMap) {
+    Set<String> methods = toMethodSet(ruleMap.get("methods"));
+    Set<String> paths = toStringSet(ruleMap.get("paths"));
+    Set<String> prefixes = toStringSet(ruleMap.get("prefixes"));
+    String resourceTypeValue = requireString(ruleMap, "resource-type");
+    String privilegeValue = requireString(ruleMap, "privilege");
+    String resourceId =
+        Optional.ofNullable(ruleMap.get("resource-id"))
+            .map(String::valueOf)
+            .map(String::trim)
+            .filter(value -> !value.isEmpty())
+            .orElse(AuthorizationRequest.GLOBAL_RESOURCE_ID);
+    Preconditions.checkArgument(
+        !paths.isEmpty() || !prefixes.isEmpty(),
+        "Dashboard privilege mapping requires at least one path or prefix");
+
+    AuthorizationRequest request =
+        AuthorizationRequest.of(
+            ResourceType.valueOf(resourceTypeValue.trim().toUpperCase()),
+            resourceId,
+            Privilege.valueOf(privilegeValue.trim().toUpperCase()));
+    return new DashboardPrivilegeMappingRule(methods, paths, prefixes, 
request);
+  }
+
+  private static Set<String> toStringSet(Object value) {
+    if (value == null) {
+      return Collections.emptySet();
+    }
+    if (!(value instanceof List)) {
+      throw new IllegalArgumentException(
+          "Invalid dashboard privilege mapping resource: expected list but 
found " + value);
+    }
+    return ((List<?>) value)
+        .stream()
+            .map(String::valueOf)
+            .map(String::trim)
+            .filter(item -> !item.isEmpty())
+            .collect(Collectors.toCollection(LinkedHashSet::new));
+  }
+
+  private static Set<String> toMethodSet(Object value) {
+    return toStringSet(value).stream()
+        .map(String::toUpperCase)
+        .collect(Collectors.toCollection(LinkedHashSet::new));
+  }
+
+  private static String requireString(Map<String, Object> source, String key) {
+    Object value = source.get(key);
+    Preconditions.checkArgument(value != null, "Missing required mapping 
field: %s", key);
+    String text = String.valueOf(value).trim();
+    Preconditions.checkArgument(!text.isEmpty(), "Missing required mapping 
field: %s", key);
+    return text;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Map<String, Object> castMap(Object value, String label) {
+    if (!(value instanceof Map)) {
+      throw new IllegalArgumentException(
+          "Invalid dashboard privilege mapping resource: " + label + " must be 
a map");
+    }
+    return (Map<String, Object>) value;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Map<String, Object> loadYaml(String resource) {
+    Yaml yaml = new Yaml();
+    String content = readResource(resource);
+    Object loaded = yaml.load(content);
+    if (!(loaded instanceof Map)) {
+      throw new IllegalArgumentException(
+          "Invalid dashboard privilege mapping resource: root must be a map");
+    }
+    return (Map<String, Object>) loaded;
+  }
+
+  private static String readResource(String resource) {
+    try (InputStream inputStream =
+            
DashboardPrivilegeMapper.class.getClassLoader().getResourceAsStream(resource);
+        InputStreamReader inputStreamReader =
+            new InputStreamReader(
+                Objects.requireNonNull(inputStream, "Missing resource: " + 
resource),
+                StandardCharsets.UTF_8);
+        BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) 
{
+      return 
bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()));
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to load authorization resource: " + 
resource, e);
+    }
+  }
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/DashboardPrivilegeMappingRule.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/DashboardPrivilegeMappingRule.java
new file mode 100644
index 000000000..c726625ee
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/DashboardPrivilegeMappingRule.java
@@ -0,0 +1,50 @@
+/*
+ * 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.amoro.server.authorization;
+
+import java.util.Collections;
+import java.util.Set;
+
+class DashboardPrivilegeMappingRule {
+  private final Set<String> methods;
+  private final Set<String> paths;
+  private final Set<String> prefixes;
+  private final AuthorizationRequest authorizationRequest;
+
+  DashboardPrivilegeMappingRule(
+      Set<String> methods,
+      Set<String> paths,
+      Set<String> prefixes,
+      AuthorizationRequest authorizationRequest) {
+    this.methods = methods == null ? Collections.emptySet() : methods;
+    this.paths = paths == null ? Collections.emptySet() : paths;
+    this.prefixes = prefixes == null ? Collections.emptySet() : prefixes;
+    this.authorizationRequest = authorizationRequest;
+  }
+
+  boolean matches(String method, String path) {
+    boolean methodMatched = methods.isEmpty() || methods.contains(method);
+    boolean pathMatched = paths.contains(path) || 
prefixes.stream().anyMatch(path::startsWith);
+    return methodMatched && pathMatched;
+  }
+
+  AuthorizationRequest getAuthorizationRequest() {
+    return authorizationRequest;
+  }
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/LdapGroupRoleResolver.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/LdapGroupRoleResolver.java
new file mode 100644
index 000000000..9f2829683
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/LdapGroupRoleResolver.java
@@ -0,0 +1,239 @@
+/*
+ * 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.amoro.server.authorization;
+
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.apache.amoro.server.utils.PreconditionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.InitialDirContext;
+
+import java.text.MessageFormat;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+class LdapGroupRoleResolver {
+  private static final Logger LOG = 
LoggerFactory.getLogger(LdapGroupRoleResolver.class);
+
+  interface GroupMemberLoader {
+    Set<String> loadMembers(String groupDn, String memberAttribute) throws 
NamingException;
+  }
+
+  private final boolean enabled;
+  private final String memberAttribute;
+  private final MessageFormat userDnFormatter;
+  private final Map<String, String> groupRoleMappings;
+  private final GroupMemberLoader groupMemberLoader;
+
+  static LdapGroupRoleResolver disabled() {
+    return new LdapGroupRoleResolver();
+  }
+
+  LdapGroupRoleResolver(Configurations conf) {
+    this(conf, new JndiGroupMemberLoader(conf));
+  }
+
+  private LdapGroupRoleResolver() {
+    this.enabled = false;
+    this.memberAttribute = "";
+    this.userDnFormatter = new MessageFormat("{0}");
+    this.groupRoleMappings = Collections.emptyMap();
+    this.groupMemberLoader = (groupDn, attribute) -> Collections.emptySet();
+  }
+
+  LdapGroupRoleResolver(Configurations conf, GroupMemberLoader 
groupMemberLoader) {
+    this.enabled = 
conf.get(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_ENABLED);
+    this.groupMemberLoader = groupMemberLoader;
+    if (!enabled) {
+      this.memberAttribute = "";
+      this.userDnFormatter = new MessageFormat("{0}");
+      this.groupRoleMappings = Collections.emptyMap();
+      return;
+    }
+
+    String ldapUrl = 
conf.get(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL);
+    PreconditionUtils.checkNotNullOrEmpty(
+        ldapUrl, AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL.key());
+
+    this.memberAttribute =
+        
conf.get(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_GROUP_MEMBER_ATTRIBUTE);
+    PreconditionUtils.checkNotNullOrEmpty(
+        memberAttribute,
+        
AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_GROUP_MEMBER_ATTRIBUTE.key());
+
+    String userDnPattern =
+        
conf.get(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_USER_DN_PATTERN);
+    PreconditionUtils.checkNotNullOrEmpty(
+        userDnPattern, 
AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_USER_DN_PATTERN.key());
+    this.userDnFormatter = new MessageFormat(userDnPattern);
+    this.groupRoleMappings = loadGroupRoleMappings(conf);
+  }
+
+  Set<String> resolve(String username) {
+    if (!enabled) {
+      return Collections.emptySet();
+    }
+
+    String userDn = userDnFormatter.format(new String[] {username});
+    Set<String> resolvedRoles = new HashSet<>();
+    for (Map.Entry<String, String> entry : groupRoleMappings.entrySet()) {
+      String groupDn = entry.getKey();
+      try {
+        Set<String> members = groupMemberLoader.loadMembers(groupDn, 
memberAttribute);
+        if (members.stream().anyMatch(member -> matchesMember(username, 
userDn, member))) {
+          resolvedRoles.add(entry.getValue());
+        }
+      } catch (NamingException e) {
+        LOG.error("Failed to query LDAP group {} for user {}", groupDn, 
username, e);
+        throw new RuntimeException(
+            "LDAP role resolution failed while querying group '" + groupDn + 
"'", e);
+      }
+    }
+    return resolvedRoles;
+  }
+
+  private static Map<String, String> loadGroupRoleMappings(Configurations 
conf) {
+    List<Map<String, String>> groups =
+        
conf.getOptional(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_GROUPS)
+            .orElse(Collections.emptyList());
+    return groups.stream()
+        .filter(LdapGroupRoleResolver::hasRequiredGroupFields)
+        .collect(
+            Collectors.toMap(
+                group -> String.valueOf(group.get("group-dn")).trim(),
+                group ->
+                    parseRoleName(
+                        String.valueOf(group.get("group-dn")), 
String.valueOf(group.get("role"))),
+                (existing, replacement) -> replacement,
+                LinkedHashMap::new));
+  }
+
+  private static boolean hasRequiredGroupFields(Map<String, String> group) {
+    if (group.get("group-dn") == null || group.get("role") == null) {
+      LOG.warn(
+          "Ignore invalid http-server.authorization.ldap-role-mapping.groups 
entry: {}", group);
+      return false;
+    }
+    return true;
+  }
+
+  private static String parseRoleName(String groupDn, String roleValue) {
+    String normalizedRole = roleValue == null ? "" : roleValue.trim();
+    if (normalizedRole.isEmpty()) {
+      throw new IllegalArgumentException(
+          String.format("Invalid empty role configured for LDAP group mapping 
'%s'", groupDn));
+    }
+    return normalizedRole;
+  }
+
+  private static boolean matchesMember(String username, String userDn, String 
member) {
+    if (member == null) {
+      return false;
+    }
+
+    String normalized = member.trim();
+    // Support DN-style, plain username, and uid= prefix formats commonly 
returned by LDAP groups.
+    return normalized.equalsIgnoreCase(userDn)
+        || normalized.equalsIgnoreCase(username)
+        || normalized.equalsIgnoreCase("uid=" + username)
+        || extractCnFromDn(normalized).equalsIgnoreCase(username);
+  }
+
+  private static String extractCnFromDn(String dn) {
+    if (dn.toUpperCase().startsWith("CN=")) {
+      int commaIdx = dn.indexOf(',');
+      return commaIdx > 0 ? dn.substring(3, commaIdx) : dn.substring(3);
+    }
+    return "";
+  }
+
+  private static class JndiGroupMemberLoader implements GroupMemberLoader {
+    private final String ldapUrl;
+    private final String bindDn;
+    private final String bindPassword;
+
+    private JndiGroupMemberLoader(Configurations conf) {
+      this.ldapUrl = 
conf.get(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL);
+      this.bindDn = 
conf.get(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_BIND_DN);
+      this.bindPassword =
+          
conf.get(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_BIND_PASSWORD);
+    }
+
+    @Override
+    public Set<String> loadMembers(String groupDn, String memberAttribute) 
throws NamingException {
+      Hashtable<String, Object> env = new Hashtable<>();
+      env.put(Context.INITIAL_CONTEXT_FACTORY, 
"com.sun.jndi.ldap.LdapCtxFactory");
+      env.put(Context.PROVIDER_URL, ldapUrl);
+      env.put("com.sun.jndi.ldap.connect.timeout", "10000");
+      env.put("com.sun.jndi.ldap.read.timeout", "10000");
+      env.put(Context.REFERRAL, "follow");
+      if (!bindDn.isEmpty()) {
+        env.put(Context.SECURITY_AUTHENTICATION, "simple");
+        env.put(Context.SECURITY_PRINCIPAL, bindDn);
+        env.put(Context.SECURITY_CREDENTIALS, bindPassword);
+      }
+
+      InitialDirContext ldapContext = null;
+      try {
+        ldapContext = new InitialDirContext(env);
+        Attributes attributes = ldapContext.getAttributes(groupDn, new 
String[] {memberAttribute});
+        return extractMembers(attributes, memberAttribute);
+      } finally {
+        if (ldapContext != null) {
+          try {
+            ldapContext.close();
+          } catch (NamingException e) {
+            LOG.warn("Failed to close LDAP role-mapping context", e);
+          }
+        }
+      }
+    }
+
+    private static Set<String> extractMembers(Attributes attributes, String 
memberAttribute)
+        throws NamingException {
+      Attribute memberValues = attributes.get(memberAttribute);
+      if (memberValues == null) {
+        return Collections.emptySet();
+      }
+
+      Set<String> members = new HashSet<>();
+      NamingEnumeration<?> values = memberValues.getAll();
+      while (values.hasMore()) {
+        Object value = values.next();
+        if (value != null) {
+          members.add(value.toString());
+        }
+      }
+      return members;
+    }
+  }
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/Privilege.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/Privilege.java
new file mode 100644
index 000000000..d7aa4cd97
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/Privilege.java
@@ -0,0 +1,31 @@
+/*
+ * 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.amoro.server.authorization;
+
+public enum Privilege {
+  VIEW_SYSTEM,
+  VIEW_CATALOG,
+  VIEW_TABLE,
+  VIEW_OPTIMIZER,
+  MANAGE_CATALOG,
+  MANAGE_TABLE,
+  MANAGE_OPTIMIZER,
+  EXECUTE_SQL,
+  MANAGE_PLATFORM
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/ResourceType.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/ResourceType.java
new file mode 100644
index 000000000..aa952ab0d
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/ResourceType.java
@@ -0,0 +1,28 @@
+/*
+ * 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.amoro.server.authorization;
+
+public enum ResourceType {
+  SYSTEM,
+  CATALOG,
+  TABLE,
+  OPTIMIZER,
+  TERMINAL,
+  PLATFORM
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/Role.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/Role.java
new file mode 100644
index 000000000..c063d3584
--- /dev/null
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/Role.java
@@ -0,0 +1,27 @@
+/*
+ * 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.amoro.server.authorization;
+
+/** Build-in Roles */
+public final class Role {
+  public static final String SERVICE_ADMIN = "SERVICE_ADMIN";
+  public static final String VIEWER = "VIEWER";
+
+  private Role() {}
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/authorization/RoleResolver.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/RoleResolver.java
new file mode 100644
index 000000000..af8971cc7
--- /dev/null
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/authorization/RoleResolver.java
@@ -0,0 +1,87 @@
+/*
+ * 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.amoro.server.authorization;
+
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public class RoleResolver {
+  private static final Logger LOG = 
LoggerFactory.getLogger(RoleResolver.class);
+
+  private final boolean authorizationEnabled;
+  private final String defaultRole;
+  private final String adminUsername;
+  private final LdapGroupRoleResolver ldapGroupRoleResolver;
+
+  public RoleResolver(Configurations serviceConfig) {
+    this(serviceConfig, createLdapGroupRoleResolver(serviceConfig));
+  }
+
+  RoleResolver(Configurations serviceConfig, LdapGroupRoleResolver 
ldapGroupRoleResolver) {
+    this.authorizationEnabled = 
serviceConfig.get(AmoroManagementConf.AUTHORIZATION_ENABLED);
+    this.defaultRole =
+        serviceConfig
+            .getOptional(AmoroManagementConf.AUTHORIZATION_DEFAULT_ROLE)
+            .map(String::trim)
+            .filter(role -> !role.isEmpty())
+            .orElse(null);
+    this.adminUsername = serviceConfig.get(AmoroManagementConf.ADMIN_USERNAME);
+    this.ldapGroupRoleResolver = ldapGroupRoleResolver;
+  }
+
+  private static LdapGroupRoleResolver 
createLdapGroupRoleResolver(Configurations serviceConfig) {
+    boolean authorizationEnabled = 
serviceConfig.get(AmoroManagementConf.AUTHORIZATION_ENABLED);
+    boolean ldapRoleMappingEnabled =
+        
serviceConfig.get(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_ENABLED);
+    if (!authorizationEnabled && ldapRoleMappingEnabled) {
+      LOG.warn(
+          "Ignore http-server.authorization.ldap-role-mapping configuration 
because http-server.authorization.enabled is false");
+    }
+    return authorizationEnabled
+        ? new LdapGroupRoleResolver(serviceConfig)
+        : LdapGroupRoleResolver.disabled();
+  }
+
+  public boolean isAuthorizationEnabled() {
+    return authorizationEnabled;
+  }
+
+  public Set<String> resolve(String username) {
+    if (!authorizationEnabled) {
+      return Collections.singleton(Role.SERVICE_ADMIN);
+    }
+
+    Set<String> roles = new LinkedHashSet<>();
+    if (adminUsername.equals(username)) {
+      roles.add(Role.SERVICE_ADMIN);
+    }
+    roles.addAll(ldapGroupRoleResolver.resolve(username));
+    if (roles.isEmpty() && defaultRole != null) {
+      roles.add(defaultRole);
+    }
+
+    return Collections.unmodifiableSet(roles);
+  }
+}
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
index e465e65e7..2246cf68b 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
@@ -39,6 +39,10 @@ import org.apache.amoro.server.AmoroManagementConf;
 import org.apache.amoro.server.AmoroServiceContainer;
 import org.apache.amoro.server.RestCatalogService;
 import org.apache.amoro.server.authentication.HttpAuthenticationFactory;
+import org.apache.amoro.server.authorization.AuthorizationRequest;
+import org.apache.amoro.server.authorization.CasbinAuthorizationManager;
+import org.apache.amoro.server.authorization.DashboardPrivilegeMapper;
+import org.apache.amoro.server.authorization.RoleResolver;
 import org.apache.amoro.server.catalog.CatalogManager;
 import org.apache.amoro.server.dashboard.controller.ApiTokenController;
 import org.apache.amoro.server.dashboard.controller.CatalogController;
@@ -78,6 +82,8 @@ public class DashboardServer {
   private static final String AUTH_TYPE_JWT = "jwt";
   private static final String X_REQUEST_SOURCE_HEADER = "X-Request-Source";
   private static final String X_REQUEST_SOURCE_WEB = "Web";
+  private static final String LOGIN_REQUIRED_MESSAGE = "Please login first";
+  private static final String NO_PERMISSION_MESSAGE = "No permission";
   private final CatalogController catalogController;
   private final HealthCheckController healthCheckController;
   private final LoginController loginController;
@@ -94,6 +100,8 @@ public class DashboardServer {
   private final PasswdAuthenticationProvider basicAuthProvider;
   private final TokenAuthenticationProvider jwtAuthProvider;
   private final String proxyClientIpHeader;
+  private final CasbinAuthorizationManager authorizationManager;
+  private final DashboardPrivilegeMapper privilegeMapper;
 
   public DashboardServer(
       Configurations serviceConfig,
@@ -105,7 +113,9 @@ public class DashboardServer {
     PlatformFileManager platformFileManager = new PlatformFileManager();
     this.catalogController = new CatalogController(catalogManager, 
platformFileManager);
     this.healthCheckController = new HealthCheckController(ams);
-    this.loginController = new LoginController(serviceConfig);
+    RoleResolver roleResolver = new RoleResolver(serviceConfig);
+    this.authorizationManager = new CasbinAuthorizationManager(serviceConfig);
+    this.loginController = new LoginController(serviceConfig, roleResolver, 
authorizationManager);
     this.optimizerGroupController = new OptimizerGroupController(tableManager, 
optimizerManager);
     this.optimizerController = new OptimizerController(optimizerManager);
     this.platformFileInfoController = new 
PlatformFileInfoController(platformFileManager);
@@ -120,6 +130,7 @@ public class DashboardServer {
     this.overviewController = new OverviewController(manager);
     APITokenManager apiTokenManager = new APITokenManager();
     this.apiTokenController = new ApiTokenController(apiTokenManager);
+    this.privilegeMapper = new DashboardPrivilegeMapper();
 
     String authType = 
serviceConfig.get(AmoroManagementConf.HTTP_SERVER_REST_AUTH_TYPE);
     this.basicAuthProvider =
@@ -408,8 +419,30 @@ public class DashboardServer {
     boolean isWebRequest = 
X_REQUEST_SOURCE_WEB.equalsIgnoreCase(requestSource);
 
     if (isWebRequest) {
-      if (null == ctx.sessionAttribute("user")) {
-        throw new ForbiddenException("User session attribute is missed for 
url: " + uriPath);
+      LoginController.SessionInfo user = ctx.sessionAttribute("user");
+      if (user == null) {
+        throw new ForbiddenException(LOGIN_REQUIRED_MESSAGE);
+      }
+      if (authorizationManager.isAuthorizationEnabled()) {
+        AuthorizationRequest request =
+            privilegeMapper
+                .resolve(ctx)
+                .orElseThrow(
+                    () ->
+                        new ForbiddenException(
+                            "No authorization mapping for request: "
+                                + ctx.method()
+                                + " "
+                                + uriPath));
+        if (!authorizationManager.authorize(user.getRoles(), request)) {
+          LOG.warn(
+              "Reject unauthorized request for user {}, URI: {}, method: {}, 
roles: {}",
+              user.getUserName(),
+              uriPath,
+              ctx.method(),
+              user.getRoles());
+          throw new ForbiddenException(NO_PERMISSION_MESSAGE);
+        }
       }
       return;
     }
@@ -439,7 +472,7 @@ public class DashboardServer {
       if (!ctx.req.getRequestURI().startsWith("/api/ams")) {
         ctx.html(getIndexFileContent());
       } else {
-        ctx.json(new ErrorResponse(HttpCode.FORBIDDEN, "Please login first", 
""));
+        ctx.json(new ErrorResponse(HttpCode.FORBIDDEN, e.getMessage(), ""));
       }
     } else if (e instanceof SignatureCheckException) {
       ctx.json(new ErrorResponse(HttpCode.FORBIDDEN, "Signature check failed", 
""));
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
index 4d39705c0..4006d3df7 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java
@@ -24,24 +24,38 @@ import org.apache.amoro.config.Configurations;
 import org.apache.amoro.server.AmoroManagementConf;
 import org.apache.amoro.server.authentication.DefaultPasswordCredential;
 import org.apache.amoro.server.authentication.HttpAuthenticationFactory;
+import org.apache.amoro.server.authorization.CasbinAuthorizationManager;
+import org.apache.amoro.server.authorization.Role;
+import org.apache.amoro.server.authorization.RoleResolver;
 import org.apache.amoro.server.dashboard.response.OkResponse;
 import org.apache.amoro.server.utils.PreconditionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.Serializable;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.Map;
+import java.util.Set;
 
 /** The controller that handles login requests. */
 public class LoginController {
   public static final Logger LOG = 
LoggerFactory.getLogger(LoginController.class);
 
   private final PasswdAuthenticationProvider loginAuthProvider;
+  private final RoleResolver roleResolver;
+  private final CasbinAuthorizationManager authorizationManager;
 
-  public LoginController(Configurations serviceConfig) {
+  public LoginController(
+      Configurations serviceConfig,
+      RoleResolver roleResolver,
+      CasbinAuthorizationManager authorizationManager) {
     this.loginAuthProvider =
         HttpAuthenticationFactory.getPasswordAuthenticationProvider(
             
serviceConfig.get(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_PROVIDER), 
serviceConfig);
+    this.roleResolver = roleResolver;
+    this.authorizationManager = authorizationManager;
   }
 
   /** Get current user. */
@@ -59,15 +73,37 @@ public class LoginController {
     PreconditionUtils.checkNotNullOrEmpty(user, "user");
     PreconditionUtils.checkNotNullOrEmpty(pwd, "password");
     DefaultPasswordCredential credential = new DefaultPasswordCredential(user, 
pwd);
+
+    // Step 1: Authenticate the user
+    Principal principal;
     try {
-      this.loginAuthProvider.authenticate(credential);
-      ctx.sessionAttribute("user", new SessionInfo(user, 
System.currentTimeMillis() + ""));
-      ctx.json(OkResponse.of("success"));
+      principal = this.loginAuthProvider.authenticate(credential);
     } catch (Exception e) {
       LOG.error("authenticate user {} failed", user, e);
       String causeMessage = e.getMessage() != null ? e.getMessage() : "unknown 
error";
       throw new RuntimeException("invalid user " + user + " or password! 
Cause: " + causeMessage);
     }
+
+    // Step 2: Resolve user role (LDAP group lookup)
+    String authenticatedUser = principal.getName();
+    Set<String> roles;
+    try {
+      roles = roleResolver.resolve(authenticatedUser);
+    } catch (Exception e) {
+      LOG.error(
+          "Role resolution failed for user {}. "
+              + "Authentication succeeded but LDAP group query failed. "
+              + "Check authorization.ldap-role-mapping config 
(bind-dn/bind-password/admin-group-dn).",
+          authenticatedUser,
+          e);
+      throw new RuntimeException("Login failed due to a server error during 
role resolution");
+    }
+
+    Set<String> privileges = authorizationManager.resolvePrivileges(roles);
+    SessionInfo sessionInfo =
+        new SessionInfo(authenticatedUser, System.currentTimeMillis() + "", 
roles, privileges);
+    ctx.sessionAttribute("user", sessionInfo);
+    ctx.json(OkResponse.of(sessionInfo));
   }
 
   /** handle logout post request. */
@@ -76,29 +112,43 @@ public class LoginController {
     ctx.json(OkResponse.ok());
   }
 
-  static class SessionInfo implements Serializable {
+  /** Session user payload persisted in the server-side HTTP session. */
+  public static class SessionInfo implements Serializable {
     String userName;
     String loginTime;
+    Set<String> roles;
+    Set<String> privileges;
 
-    public SessionInfo(String username, String loginTime) {
+    public SessionInfo(
+        String username, String loginTime, Set<String> roles, Set<String> 
privileges) {
       this.userName = username;
       this.loginTime = loginTime;
+      this.roles = Collections.unmodifiableSet(new LinkedHashSet<>(roles));
+      this.privileges = Collections.unmodifiableSet(new 
LinkedHashSet<>(privileges));
     }
 
     public String getUserName() {
       return userName;
     }
 
-    public void setUserName(String userName) {
-      this.userName = userName;
-    }
-
     public String getLoginTime() {
       return loginTime;
     }
 
-    public void setLoginTime(String loginTime) {
-      this.loginTime = loginTime;
+    public Set<String> getRoles() {
+      return roles;
+    }
+
+    public Set<String> getPrivileges() {
+      return privileges;
+    }
+
+    public String getRole() {
+      if (roles.contains(Role.SERVICE_ADMIN)) {
+        return Role.SERVICE_ADMIN;
+      }
+
+      return roles.stream().findFirst().orElse(null);
     }
   }
 }
diff --git a/amoro-ams/src/main/resources/authorization/amoro_rbac_model.conf 
b/amoro-ams/src/main/resources/authorization/amoro_rbac_model.conf
new file mode 100644
index 000000000..4a755e9a3
--- /dev/null
+++ b/amoro-ams/src/main/resources/authorization/amoro_rbac_model.conf
@@ -0,0 +1,28 @@
+# 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.
+
+[request_definition]
+r = sub, resType, resId, act
+
+[policy_definition]
+p = sub, resType, resId, act, eft
+
+[policy_effect]
+e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
+
+[matchers]
+m = r.sub == p.sub && r.resType == p.resType && r.resId == p.resId && r.act == 
p.act
diff --git a/amoro-ams/src/main/resources/authorization/amoro_rbac_policy.csv 
b/amoro-ams/src/main/resources/authorization/amoro_rbac_policy.csv
new file mode 100644
index 000000000..58d073260
--- /dev/null
+++ b/amoro-ams/src/main/resources/authorization/amoro_rbac_policy.csv
@@ -0,0 +1,32 @@
+# 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.
+
+# Default viewer policy
+p, VIEWER, CATALOG, GLOBAL, VIEW_CATALOG, allow
+p, VIEWER, TABLE, GLOBAL, VIEW_TABLE, allow
+p, VIEWER, OPTIMIZER, GLOBAL, VIEW_OPTIMIZER, allow
+
+# Service admin policy
+p, SERVICE_ADMIN, SYSTEM, GLOBAL, VIEW_SYSTEM, allow
+p, SERVICE_ADMIN, CATALOG, GLOBAL, VIEW_CATALOG, allow
+p, SERVICE_ADMIN, TABLE, GLOBAL, VIEW_TABLE, allow
+p, SERVICE_ADMIN, OPTIMIZER, GLOBAL, VIEW_OPTIMIZER, allow
+p, SERVICE_ADMIN, CATALOG, GLOBAL, MANAGE_CATALOG, allow
+p, SERVICE_ADMIN, TABLE, GLOBAL, MANAGE_TABLE, allow
+p, SERVICE_ADMIN, OPTIMIZER, GLOBAL, MANAGE_OPTIMIZER, allow
+p, SERVICE_ADMIN, TERMINAL, GLOBAL, EXECUTE_SQL, allow
+p, SERVICE_ADMIN, PLATFORM, GLOBAL, MANAGE_PLATFORM, allow
diff --git a/amoro-ams/src/main/resources/authorization/privilege_mapping.yaml 
b/amoro-ams/src/main/resources/authorization/privilege_mapping.yaml
new file mode 100644
index 000000000..b0a32fe9a
--- /dev/null
+++ b/amoro-ams/src/main/resources/authorization/privilege_mapping.yaml
@@ -0,0 +1,97 @@
+# 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.
+
+mappings:
+  - prefixes:
+      - /api/ams/v1/overview
+    methods:
+      - GET
+    resource-type: SYSTEM
+    privilege: VIEW_SYSTEM
+
+  - prefixes:
+      - /api/ams/v1/catalogs
+    methods:
+      - GET
+    resource-type: CATALOG
+    privilege: VIEW_CATALOG
+
+  - prefixes:
+      - /api/ams/v1/catalogs
+    methods:
+      - POST
+      - PUT
+      - DELETE
+    resource-type: CATALOG
+    privilege: MANAGE_CATALOG
+
+  - prefixes:
+      - /api/ams/v1/tables
+    methods:
+      - GET
+    resource-type: TABLE
+    privilege: VIEW_TABLE
+
+  - prefixes:
+      - /api/ams/v1/tables
+    methods:
+      - POST
+      - PUT
+      - DELETE
+    resource-type: TABLE
+    privilege: MANAGE_TABLE
+
+  - paths:
+      - /api/ams/v1/upgrade/properties
+    methods:
+      - POST
+      - PUT
+      - DELETE
+    resource-type: TABLE
+    privilege: MANAGE_TABLE
+
+  - prefixes:
+      - /api/ams/v1/optimize
+    methods:
+      - GET
+    resource-type: OPTIMIZER
+    privilege: VIEW_OPTIMIZER
+
+  - prefixes:
+      - /api/ams/v1/optimize
+    methods:
+      - POST
+      - PUT
+      - DELETE
+    resource-type: OPTIMIZER
+    privilege: MANAGE_OPTIMIZER
+
+  - prefixes:
+      - /api/ams/v1/terminal
+    methods:
+      - GET
+      - POST
+      - PUT
+    resource-type: TERMINAL
+    privilege: EXECUTE_SQL
+
+  - prefixes:
+      - /api/ams/v1/files
+      - /api/ams/v1/settings
+      - /api/ams/v1/api/token
+    resource-type: PLATFORM
+    privilege: MANAGE_PLATFORM
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/config/ConfigurationsTest.java 
b/amoro-ams/src/test/java/org/apache/amoro/config/ConfigurationsTest.java
index 1e9a9df41..bbd1a45ba 100644
--- a/amoro-ams/src/test/java/org/apache/amoro/config/ConfigurationsTest.java
+++ b/amoro-ams/src/test/java/org/apache/amoro/config/ConfigurationsTest.java
@@ -32,6 +32,8 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
@@ -59,6 +61,94 @@ public class ConfigurationsTest {
   public static String UPDATE_CMD =
       "UPDATE=1 ./mvnw test -pl amoro-ams -am -Dtest=ConfigurationsTest";
 
+  private static final List<String> RBAC_EXAMPLE =
+      Arrays.asList(
+          "## RBAC Example",
+          "",
+          "Enable RBAC only when you need role separation for dashboard 
users.",
+          "",
+          "The current RBAC model uses:",
+          "",
+          "- string-based roles",
+          "- LDAP group-to-role mapping as the primary role source",
+          "- built-in Casbin policy to translate roles into privileges",
+          "- privilege-driven frontend authorization",
+          "",
+          "Amoro provides two built-in roles by default:",
+          "",
+          "| Role | Description | Default Privileges |",
+          "| --- | --- | --- |",
+          "| `SERVICE_ADMIN` | Platform administrator | All privileges |",
+          "| `VIEWER` | Read-only resource viewer | `VIEW_CATALOG`, 
`VIEW_TABLE`, `VIEW_OPTIMIZER` |",
+          "",
+          "`VIEWER` does not include `VIEW_SYSTEM`, so it cannot access 
`Overview` or `Terminal`.",
+          "After login succeeds, `/login/current` returns both `roles` and 
effective `privileges`.",
+          "",
+          "If you need additional roles, define them by Casbin policy and map 
LDAP groups to those",
+          "role names. The role name itself does not need to be added to Java 
enum code.",
+          "",
+          "```yaml",
+          "ams:",
+          "  http-server:",
+          "    authorization:",
+          "      enabled: true",
+          "```",
+          "",
+          "```yaml",
+          "ams:",
+          "  http-server:",
+          "    login-auth-provider: 
org.apache.amoro.server.authentication.LdapPasswdAuthenticationProvider",
+          "    login-auth-ldap-url: \"ldap://ldap.example.com:389\"";,
+          "    login-auth-ldap-user-pattern: 
\"uid={0},ou=people,dc=example,dc=com\"",
+          "    authorization:",
+          "      enabled: true",
+          "      ldap-role-mapping:",
+          "        enabled: true",
+          "        group-member-attribute: \"member\"",
+          "        user-dn-pattern: \"uid={0},ou=people,dc=example,dc=com\"",
+          "        bind-dn: \"cn=service-account,dc=example,dc=com\"",
+          "        bind-password: \"service-password\"",
+          "        groups:",
+          "          - group-dn: 
\"cn=amoro-service-admins,ou=groups,dc=example,dc=com\"",
+          "            role: SERVICE_ADMIN",
+          "          - group-dn: 
\"cn=amoro-viewers,ou=groups,dc=example,dc=com\"",
+          "            role: VIEWER",
+          "          - group-dn: 
\"cn=amoro-catalog-admins,ou=groups,dc=example,dc=com\"",
+          "            role: CATALOG_ADMIN",
+          "```",
+          "",
+          "Example `/login/current` response:",
+          "",
+          "```json",
+          "{",
+          "  \"userName\": \"alice\",",
+          "  \"roles\": [\"CATALOG_ADMIN\"],",
+          "  \"privileges\": [",
+          "    \"VIEW_CATALOG\",",
+          "    \"MANAGE_CATALOG\",",
+          "    \"VIEW_TABLE\",",
+          "    \"MANAGE_TABLE\"",
+          "  ]",
+          "}",
+          "```",
+          "",
+          "Example custom role policy:",
+          "",
+          "```csv",
+          "p, CATALOG_ADMIN, CATALOG, GLOBAL, VIEW_CATALOG, allow",
+          "p, CATALOG_ADMIN, CATALOG, GLOBAL, MANAGE_CATALOG, allow",
+          "p, CATALOG_ADMIN, TABLE, GLOBAL, VIEW_TABLE, allow",
+          "p, CATALOG_ADMIN, TABLE, GLOBAL, MANAGE_TABLE, allow",
+          "```",
+          "",
+          "Notes:",
+          "",
+          "- Recommended production setup is explicit role assignment only.",
+          "- `default-role` is optional. If it is not set, users who do not 
match any role mapping get no business role.",
+          "- Use `default-role: VIEWER` only if you intentionally want 
authenticated users without a matched role mapping to receive read-only 
access.",
+          "- Casbin model and default policy are built into the service and 
loaded from classpath.",
+          "- Dashboard request-to-privilege mapping is also built into the 
service and loaded from a resource configuration file.");
+
   @Test
   public void testAmoroManagementConfDocumentation() throws Exception {
     List<AmoroConfInfo> confInfoList = new ArrayList<>();
@@ -66,7 +156,8 @@ public class ConfigurationsTest {
         new AmoroConfInfo(
             AmoroManagementConf.class,
             "Amoro Management Service Configuration",
-            "The configuration options for Amoro Management Service (AMS)."));
+            "The configuration options for Amoro Management Service (AMS).",
+            RBAC_EXAMPLE));
     confInfoList.add(
         new AmoroConfInfo(
             ConfigShadeUtils.class,
@@ -147,6 +238,12 @@ public class ConfigurationsTest {
 
       // Add some space between different configuration sections
       output.add("");
+
+      // Add appendix content if present
+      if (confInfo.appendix != null && !confInfo.appendix.isEmpty()) {
+        output.addAll(confInfo.appendix);
+      }
+
       output.add("");
     }
 
@@ -299,11 +396,21 @@ public class ConfigurationsTest {
     Class<?> confClass;
     String title;
     String description;
+    List<String> appendix;
 
     public AmoroConfInfo(Class<?> confClass, String title, String description) 
{
       this.confClass = confClass;
       this.title = title;
       this.description = description;
+      this.appendix = Collections.emptyList();
+    }
+
+    public AmoroConfInfo(
+        Class<?> confClass, String title, String description, List<String> 
appendix) {
+      this.confClass = confClass;
+      this.title = title;
+      this.description = description;
+      this.appendix = appendix;
     }
   }
 }
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/authorization/CasbinAuthorizationManagerTest.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/authorization/CasbinAuthorizationManagerTest.java
new file mode 100644
index 000000000..9e4aa5d5b
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/authorization/CasbinAuthorizationManagerTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.amoro.server.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+public class CasbinAuthorizationManagerTest {
+
+  @Test
+  public void testResolvePrivilegesForBuiltInRoles() {
+    Configurations conf = new Configurations();
+    conf.set(AmoroManagementConf.AUTHORIZATION_ENABLED, true);
+
+    CasbinAuthorizationManager manager = new CasbinAuthorizationManager(conf);
+
+    Set<String> viewerPrivileges = 
manager.resolvePrivileges(Set.of(Role.VIEWER));
+    assertTrue(viewerPrivileges.contains(Privilege.VIEW_TABLE.name()));
+    assertFalse(viewerPrivileges.contains(Privilege.VIEW_SYSTEM.name()));
+
+    Set<String> adminPrivileges = 
manager.resolvePrivileges(Set.of(Role.SERVICE_ADMIN));
+    assertTrue(adminPrivileges.contains(Privilege.VIEW_SYSTEM.name()));
+    assertTrue(adminPrivileges.contains(Privilege.EXECUTE_SQL.name()));
+  }
+}
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/authorization/LdapGroupRoleResolverTest.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/authorization/LdapGroupRoleResolverTest.java
new file mode 100644
index 000000000..e63411e30
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/authorization/LdapGroupRoleResolverTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.amoro.server.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.junit.jupiter.api.Test;
+
+import javax.naming.NamingException;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+public class LdapGroupRoleResolverTest {
+
+  @Test
+  public void testResolveRoleFromUserDnMember() {
+    LdapGroupRoleResolver resolver =
+        new LdapGroupRoleResolver(
+            baseConfig(),
+            (groupDn, memberAttribute) ->
+                
"cn=amoro-service-admins,ou=groups,dc=example,dc=com".equals(groupDn)
+                    ? 
Collections.singleton("uid=alice,ou=people,dc=example,dc=com")
+                    : Collections.emptySet());
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("alice"));
+  }
+
+  @Test
+  public void testResolveRoleFromUsernameMember() {
+    LdapGroupRoleResolver resolver =
+        new LdapGroupRoleResolver(
+            baseConfig(),
+            (groupDn, memberAttribute) ->
+                
"cn=amoro-service-admins,ou=groups,dc=example,dc=com".equals(groupDn)
+                    ? Set.of("alice")
+                    : Collections.emptySet());
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("alice"));
+  }
+
+  @Test
+  public void testResolveRoleFromCnDnMember() {
+    LdapGroupRoleResolver resolver =
+        new LdapGroupRoleResolver(
+            baseConfig(),
+            (groupDn, memberAttribute) ->
+                
"cn=amoro-service-admins,ou=groups,dc=example,dc=com".equals(groupDn)
+                    ? Set.of("CN=alice,OU=Employees,OU=Cisco 
Users,DC=cisco,DC=com")
+                    : Collections.emptySet());
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("alice"));
+  }
+
+  @Test
+  public void testResolveRoleFromMemberUidStyleValue() {
+    LdapGroupRoleResolver resolver =
+        new LdapGroupRoleResolver(
+            baseConfig(),
+            (groupDn, memberAttribute) ->
+                
"cn=amoro-service-admins,ou=groups,dc=example,dc=com".equals(groupDn)
+                    ? Set.of("uid=alice")
+                    : Collections.emptySet());
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("alice"));
+  }
+
+  @Test
+  public void testResolveRoleIsCaseInsensitiveForDnMembers() {
+    LdapGroupRoleResolver resolver =
+        new LdapGroupRoleResolver(
+            baseConfig(),
+            (groupDn, memberAttribute) ->
+                
"cn=amoro-service-admins,ou=groups,dc=example,dc=com".equals(groupDn)
+                    ? Set.of("UID=Alice,OU=People,DC=Example,DC=Com")
+                    : Collections.emptySet());
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("alice"));
+  }
+
+  @Test
+  public void testResolveViewerGroupRole() {
+    LdapGroupRoleResolver resolver =
+        new LdapGroupRoleResolver(
+            baseConfig(),
+            (groupDn, memberAttribute) ->
+                "cn=amoro-viewers,ou=groups,dc=example,dc=com".equals(groupDn)
+                    ? Set.of("bob")
+                    : Collections.emptySet());
+
+    assertEquals(Collections.singleton(Role.VIEWER), resolver.resolve("bob"));
+  }
+
+  @Test
+  public void testThrowsWhenLookupFails() {
+    LdapGroupRoleResolver resolver =
+        new LdapGroupRoleResolver(
+            baseConfig(),
+            (groupDn, memberAttribute) -> {
+              throw new NamingException("ldap unavailable");
+            });
+
+    RuntimeException ex = assertThrows(RuntimeException.class, () -> 
resolver.resolve("alice"));
+    assertTrue(ex.getMessage().contains("LDAP role resolution failed"));
+  }
+
+  @Test
+  public void testRequireLdapGroupConfigWhenEnabled() {
+    Configurations conf = new Configurations();
+    conf.set(AmoroManagementConf.AUTHORIZATION_ENABLED, true);
+    conf.set(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_ENABLED, 
true);
+    conf.set(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL, 
"ldap://ldap.example.com:389";);
+
+    assertThrows(IllegalArgumentException.class, () -> new 
LdapGroupRoleResolver(conf));
+  }
+
+  private static Configurations baseConfig() {
+    Configurations conf = new Configurations();
+    conf.set(AmoroManagementConf.AUTHORIZATION_ENABLED, true);
+    conf.set(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_ENABLED, 
true);
+    conf.set(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL, 
"ldap://ldap.example.com:389";);
+    conf.set(
+        AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_USER_DN_PATTERN,
+        "uid={0},ou=people,dc=example,dc=com");
+    conf.set(
+        AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_GROUPS,
+        Arrays.asList(
+            group("cn=amoro-service-admins,ou=groups,dc=example,dc=com", 
"SERVICE_ADMIN"),
+            group("cn=amoro-viewers,ou=groups,dc=example,dc=com", "VIEWER")));
+    return conf;
+  }
+
+  private static Map<String, String> group(String groupDn, String role) {
+    return Map.of("group-dn", groupDn, "role", role);
+  }
+}
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/authorization/RoleResolverTest.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/authorization/RoleResolverTest.java
new file mode 100644
index 000000000..4b85ddec0
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/authorization/RoleResolverTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.amoro.server.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.server.AmoroManagementConf;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+public class RoleResolverTest {
+
+  @Test
+  public void testDisabledAuthorizationDefaultsToServiceAdmin() {
+    Configurations conf = new Configurations();
+
+    RoleResolver resolver = new RoleResolver(conf);
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("viewer"));
+  }
+
+  @Test
+  public void testResolveBootstrapAdminAndDefaultViewerRole() {
+    Configurations conf = new Configurations();
+    conf.set(AmoroManagementConf.ADMIN_USERNAME, "admin");
+    conf.set(AmoroManagementConf.AUTHORIZATION_ENABLED, true);
+    conf.set(AmoroManagementConf.AUTHORIZATION_DEFAULT_ROLE, Role.VIEWER);
+
+    RoleResolver resolver = new RoleResolver(conf);
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("admin"));
+    assertEquals(Collections.singleton(Role.VIEWER), 
resolver.resolve("alice"));
+  }
+
+  @Test
+  public void testResolveLdapGroupRoles() {
+    Configurations conf = baseConfig();
+    RoleResolver resolver =
+        new RoleResolver(
+            conf,
+            new LdapGroupRoleResolver(
+                conf,
+                (groupDn, memberAttribute) ->
+                    
"cn=amoro-service-admins,ou=groups,dc=example,dc=com".equals(groupDn)
+                        ? 
Collections.singleton("uid=alice,ou=people,dc=example,dc=com")
+                        : Collections.emptySet()));
+
+    assertEquals(Collections.singleton(Role.SERVICE_ADMIN), 
resolver.resolve("alice"));
+    assertEquals(Collections.singleton(Role.VIEWER), resolver.resolve("bob"));
+  }
+
+  @Test
+  public void testDefaultRoleOnlyAppliesWhenNoExplicitRoleMatched() {
+    Configurations conf = baseConfig();
+    conf.set(AmoroManagementConf.AUTHORIZATION_DEFAULT_ROLE, Role.VIEWER);
+    RoleResolver resolver =
+        new RoleResolver(
+            conf,
+            new LdapGroupRoleResolver(
+                conf,
+                (groupDn, memberAttribute) ->
+                    
"cn=amoro-service-admins,ou=groups,dc=example,dc=com".equals(groupDn)
+                        ? 
Collections.singleton("uid=admin,ou=people,dc=example,dc=com")
+                        : Collections.emptySet()));
+
+    Set<String> adminRoles = resolver.resolve("admin");
+    assertEquals(1, adminRoles.size());
+    assertTrue(adminRoles.contains(Role.SERVICE_ADMIN));
+  }
+
+  private static Configurations baseConfig() {
+    Configurations conf = new Configurations();
+    conf.set(AmoroManagementConf.AUTHORIZATION_ENABLED, true);
+    conf.set(AmoroManagementConf.HTTP_SERVER_LOGIN_AUTH_LDAP_URL, 
"ldap://ldap.example.com:389";);
+    conf.set(AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_ENABLED, 
true);
+    conf.set(
+        AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_USER_DN_PATTERN,
+        "uid={0},ou=people,dc=example,dc=com");
+    conf.set(
+        AmoroManagementConf.AUTHORIZATION_LDAP_ROLE_MAPPING_GROUPS,
+        Arrays.asList(
+            group("cn=amoro-service-admins,ou=groups,dc=example,dc=com", 
"SERVICE_ADMIN"),
+            group("cn=amoro-viewers,ou=groups,dc=example,dc=com", "VIEWER")));
+    conf.set(AmoroManagementConf.AUTHORIZATION_DEFAULT_ROLE, Role.VIEWER);
+    return conf;
+  }
+
+  private static Map<String, String> group(String groupDn, String role) {
+    return Map.of("group-dn", groupDn, "role", role);
+  }
+}
diff --git a/amoro-web/mock/modules/common.js b/amoro-web/mock/modules/common.js
index 089dea244..8a2267fd2 100644
--- a/amoro-web/mock/modules/common.js
+++ b/amoro-web/mock/modules/common.js
@@ -25,7 +25,9 @@ export default [
       msg: 'success',
       "result": {
         "userName": "admin",
-        "loginTime": "1703839452053"
+        "loginTime": "1703839452053",
+        "role": "SERVICE_ADMIN",
+        "roles": ["SERVICE_ADMIN"]
       }
     }),
   },
@@ -35,7 +37,12 @@ export default [
     response: () => ({
       code: 200,
       msg: 'success',
-      result: 'success'
+      result: {
+        userName: 'admin',
+        loginTime: '1703839452053',
+        role: 'SERVICE_ADMIN',
+        roles: ['SERVICE_ADMIN']
+      }
     }),
   },
   {
diff --git a/amoro-web/src/components/Sidebar.vue 
b/amoro-web/src/components/Sidebar.vue
index 31cf72bee..f5cf27526 100644
--- a/amoro-web/src/components/Sidebar.vue
+++ b/amoro-web/src/components/Sidebar.vue
@@ -22,6 +22,15 @@ import { useRoute, useRouter } from 'vue-router'
 import { useI18n } from 'vue-i18n'
 import useStore from '@/store/index'
 import { getQueryString } from '@/utils'
+import {
+  canExecuteSql,
+  canManagePlatform,
+  canViewCatalog,
+  canViewOptimizer,
+  canViewSystem,
+  canViewTable,
+  getDefaultRoute,
+} from '@/utils/permission'
 
 interface MenuItem {
   key: string
@@ -44,6 +53,12 @@ export default defineComponent({
     const hasToken = computed(() => {
       return !!(getQueryString('token') || '')
     })
+    const canAccessSystem = computed(() => canViewSystem())
+    const canAccessCatalog = computed(() => canViewCatalog())
+    const canAccessTable = computed(() => canViewTable())
+    const canAccessOptimizer = computed(() => canViewOptimizer())
+    const canAccessTerminal = computed(() => canExecuteSql())
+    const canAccessSettings = computed(() => canManagePlatform())
     const menuList = computed(() => {
       const menu: MenuItem[] = [
         {
@@ -84,7 +99,28 @@ export default defineComponent({
           icon: 'settings',
         },
       ]
-      return hasToken.value ? menu : allMenu
+      const source = hasToken.value ? menu : allMenu
+      return source.filter((item) => {
+        if (!canAccessSettings.value && item.key === 'settings') {
+          return false
+        }
+        if (!canAccessSystem.value && item.key === 'overview') {
+          return false
+        }
+        if (!canAccessTerminal.value && item.key === 'terminal') {
+          return false
+        }
+        if (!canAccessCatalog.value && item.key === 'catalogs') {
+          return false
+        }
+        if (!canAccessOptimizer.value && item.key === 'optimizing') {
+          return false
+        }
+        if (!canAccessTable.value && item.key === 'tables') {
+          return false
+        }
+        return true
+      })
     })
 
     const setCurMenu = () => {
@@ -157,13 +193,19 @@ export default defineComponent({
 
     const viewOverview = () => {
       router.push({
-        path: '/overview',
+        path: canAccessSystem.value ? '/overview' : getDefaultRoute(),
       })
     }
 
     return {
       ...toRefs(state),
       hasToken,
+      canAccessSystem,
+      canAccessCatalog,
+      canAccessTable,
+      canAccessOptimizer,
+      canAccessTerminal,
+      canAccessSettings,
       menuList,
       toggleCollapsed,
       navClick,
diff --git a/amoro-web/src/components/Topbar.vue 
b/amoro-web/src/components/Topbar.vue
index 3e24f1a66..44abc0966 100644
--- a/amoro-web/src/components/Topbar.vue
+++ b/amoro-web/src/components/Topbar.vue
@@ -40,6 +40,9 @@ const verInfo = reactive<IVersion>({
 const { t, locale } = useI18n()
 const router = useRouter()
 const store = useStore()
+function roleText() {
+  return (store.userInfo.roles || []).join(', ') || '-'
+}
 
 async function getVersion() {
   const res = await getVersionInfo()
@@ -65,6 +68,8 @@ async function handleLogout() {
       finally {
         store.updateUserInfo({
           userName: '',
+          roles: [],
+          privileges: [],
         })
         goLoginPage()
       }
@@ -97,7 +102,7 @@ onMounted(() => {
       <span class="g-mr-8">{{ `${$t('commitTime')}:  ${verInfo.commitTime}` 
}}</span>
     </div>
     <a-dropdown>
-      <span>{{ store.userInfo.userName }} <DownOutlined /></span>
+      <span><span class="role-badge">{{ roleText() }}</span>{{ 
store.userInfo.userName }} <DownOutlined /></span>
       <template #overlay>
         <a-menu>
           <a-menu-item key="userGuide" @click="goDocs">
@@ -141,4 +146,12 @@ onMounted(() => {
   .logout-button:hover {
     border-color: unset;
   }
+  .role-badge {
+    display: inline-block;
+    margin-right: 8px;
+    padding: 1px 6px;
+    border-radius: 10px;
+    background: #f0f5ff;
+    color: #1d39c4;
+  }
 </style>
diff --git a/amoro-web/src/main.ts b/amoro-web/src/main.ts
index d7f8da460..58dd06d8d 100644
--- a/amoro-web/src/main.ts
+++ b/amoro-web/src/main.ts
@@ -33,6 +33,16 @@ import './assets/icons'
 import loginService from './services/login.service'
 import { getQueryString } from './utils'
 import { resetLoginTip } from './utils/request'
+import {
+  canExecuteSql,
+  canManagePlatform,
+  canManageTable,
+  canViewCatalog,
+  canViewOptimizer,
+  canViewSystem,
+  canViewTable,
+  getDefaultRoute,
+} from './utils/permission'
 import SvgIcon from '@/components/svg-icon.vue'
 
 import 'virtual:svg-icons-register'
@@ -65,8 +75,13 @@ RegisterComponents(app);
     const token = getQueryString('token') || ''
     const res = await loginService.getCurUserInfo(token)
     if (res) {
+      const roles = res.roles || (res.role ? [res.role] : [])
+      const privileges = res.privileges || []
       store.updateUserInfo({
         userName: res.userName,
+        role: roles[0],
+        roles,
+        privileges,
       })
     }
   }
@@ -88,6 +103,57 @@ RegisterComponents(app);
         })
         return
       }
+      if (
+        to.path === '/settings'
+        && !canManagePlatform()
+      ) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
+      if (to.path === '/hive-tables/upgrade' && !canManageTable()) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
+      if (to.path === '/overview' && !canViewSystem()) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
+      if (to.path === '/terminal' && !canExecuteSql()) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
+      if (to.path === '/catalogs' && !canViewCatalog()) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
+      if (to.path === '/optimizing' && !canViewOptimizer()) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
+      if ((to.path === '/tables' || to.path.startsWith('/tables/')) && 
!canViewTable()) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
+      if (to.path.startsWith('/hive-tables') && !canViewTable()) {
+        next({
+          path: getDefaultRoute(),
+        })
+        return
+      }
       next()
     })
 
diff --git a/amoro-web/src/router/index.ts b/amoro-web/src/router/index.ts
index 734395410..0c3bfd4eb 100644
--- a/amoro-web/src/router/index.ts
+++ b/amoro-web/src/router/index.ts
@@ -37,7 +37,7 @@ const routes: Array<RouteRecordRaw> = [
   {
     path: '/',
     name: 'Home',
-    redirect: 'overview', // overview
+    redirect: 'tables',
     component: Home,
     children: [
       {
diff --git a/amoro-web/src/store/index.ts b/amoro-web/src/store/index.ts
index 2f02b8cf9..7a7fb7978 100644
--- a/amoro-web/src/store/index.ts
+++ b/amoro-web/src/store/index.ts
@@ -23,6 +23,8 @@ function state(): GlobalState {
   return {
     userInfo: {
       userName: '',
+      roles: [],
+      privileges: [],
     // token: ''
     } as UserInfo,
     isShowTablesMenu: false,
diff --git a/amoro-web/src/types/common.type.ts 
b/amoro-web/src/types/common.type.ts
index 073c78c58..2f5dff1c7 100644
--- a/amoro-web/src/types/common.type.ts
+++ b/amoro-web/src/types/common.type.ts
@@ -27,6 +27,9 @@ export interface IHttpResponse {
 }
 export interface UserInfo {
   userName: string
+  role?: string
+  roles?: string[]
+  privileges?: string[]
   token?: string
 }
 
diff --git a/amoro-web/src/utils/permission.ts 
b/amoro-web/src/utils/permission.ts
new file mode 100644
index 000000000..ca61d658a
--- /dev/null
+++ b/amoro-web/src/utils/permission.ts
@@ -0,0 +1,106 @@
+/*
+  * 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.
+  */
+
+import useStore from '@/store'
+
+export type UserPrivilege =
+  | 'VIEW_SYSTEM'
+  | 'VIEW_CATALOG'
+  | 'VIEW_TABLE'
+  | 'VIEW_OPTIMIZER'
+  | 'MANAGE_CATALOG'
+  | 'MANAGE_TABLE'
+  | 'MANAGE_OPTIMIZER'
+  | 'EXECUTE_SQL'
+  | 'MANAGE_PLATFORM'
+
+export function getRoles(): string[] {
+  const store = useStore()
+  const roles = store.userInfo.roles || []
+  if (roles.length) {
+    return roles
+  }
+  return store.userInfo.role ? [store.userInfo.role] : []
+}
+
+export function getPrivileges(): UserPrivilege[] {
+  const store = useStore()
+  return (store.userInfo.privileges || []) as UserPrivilege[]
+}
+
+export function hasPrivilege(privilege: UserPrivilege): boolean {
+  return getPrivileges().includes(privilege)
+}
+
+export function canViewSystem(): boolean {
+  return hasPrivilege('VIEW_SYSTEM')
+}
+
+export function canViewCatalog(): boolean {
+  return hasPrivilege('VIEW_CATALOG')
+}
+
+export function canManageCatalog(): boolean {
+  return hasPrivilege('MANAGE_CATALOG')
+}
+
+export function canViewTable(): boolean {
+  return hasPrivilege('VIEW_TABLE')
+}
+
+export function canManageTable(): boolean {
+  return hasPrivilege('MANAGE_TABLE')
+}
+
+export function canViewOptimizer(): boolean {
+  return hasPrivilege('VIEW_OPTIMIZER')
+}
+
+export function canManageOptimizer(): boolean {
+  return hasPrivilege('MANAGE_OPTIMIZER')
+}
+
+export function canExecuteSql(): boolean {
+  return hasPrivilege('EXECUTE_SQL')
+}
+
+export function canManagePlatform(): boolean {
+  return hasPrivilege('MANAGE_PLATFORM')
+}
+
+export function getDefaultRoute(): string {
+  if (canViewTable()) {
+    return '/tables'
+  }
+  if (canViewCatalog()) {
+    return '/catalogs'
+  }
+  if (canViewOptimizer()) {
+    return '/optimizing'
+  }
+  if (canViewSystem()) {
+    return '/overview'
+  }
+  if (canExecuteSql()) {
+    return '/terminal'
+  }
+  if (canManagePlatform()) {
+    return '/settings'
+  }
+  return '/login'
+}
diff --git a/amoro-web/src/utils/request.ts b/amoro-web/src/utils/request.ts
index 33beff375..0cb0683a8 100644
--- a/amoro-web/src/utils/request.ts
+++ b/amoro-web/src/utils/request.ts
@@ -159,18 +159,24 @@ const request: any = function (options: 
CustomAxiosRequestConfig) {
       }
       // not login
       if (code === 403) {
-        const store = useStore()
-        store.updateUserInfo({
-          userName: '',
-        })
-        const currentPath = router.currentRoute.value.path
-        if (requestConfig.handleError && currentPath !== '/login' && 
!loginTipShown) {
-          message.error(msg || 'need login')
-          loginTipShown = true
+        const needLogin = (msg || '').toLowerCase().includes('login')
+        if (needLogin) {
+          const store = useStore()
+          store.updateUserInfo({
+            userName: '',
+            roles: [],
+            privileges: [],
+          })
+          const currentPath = router.currentRoute.value.path
+          if (requestConfig.handleError && currentPath !== '/login' && 
!loginTipShown) {
+            message.error(msg || 'need login')
+            loginTipShown = true
+          }
+          return router.push({
+            path: '/login',
+          })
         }
-        return router.push({
-          path: '/login',
-        })
+        return Promise.reject(new Error(msg || 'No permission'))
       }
       return Promise.reject(new Error(msg || 'error'))
     })
diff --git a/amoro-web/src/views/catalogs/Detail.vue 
b/amoro-web/src/views/catalogs/Detail.vue
index 3eb09815a..634775686 100644
--- a/amoro-web/src/views/catalogs/Detail.vue
+++ b/amoro-web/src/views/catalogs/Detail.vue
@@ -34,6 +34,7 @@ import type { ICatalogItem, IIOptimizeGroupItem, 
ILableAndValue, IMap } from '@/
 import { usePlaceholder } from '@/hooks/usePlaceholder'
 import { getResourceGroupsListAPI } from '@/services/optimize.service'
 import { downloadWithHeader } from '@/utils/request'
+import { canManageCatalog } from '@/utils/permission'
 
 interface IStorageConfigItem {
   label: string
@@ -107,6 +108,7 @@ const isHiveMetastore = computed(() => {
   return formState.catalog.type === 'hive'
 })
 const loading = ref<boolean>(false)
+const writable = computed(() => canManageCatalog())
 const formRef = ref()
 const propertiesRef = ref()
 const tablePropertiesRef = ref()
@@ -778,7 +780,7 @@ onMounted(() => {
         </a-form>
       </div>
     </div>
-    <div v-if="isEdit" class="footer-btn">
+    <div v-if="isEdit && writable" class="footer-btn">
       <a-button type="primary" class="save-btn g-mr-12" @click="handleSave">
         {{ $t('save') }}
       </a-button>
@@ -786,7 +788,7 @@ onMounted(() => {
         {{ $t('cancel') }}
       </a-button>
     </div>
-    <div v-if="!isEdit" class="footer-btn">
+    <div v-if="!isEdit && writable" class="footer-btn">
       <a-button type="primary" class="edit-btn g-mr-12" @click="handleEdit">
         {{ $t('edit') }}
       </a-button>
diff --git a/amoro-web/src/views/catalogs/index.vue 
b/amoro-web/src/views/catalogs/index.vue
index 61652a45b..743d12313 100644
--- a/amoro-web/src/views/catalogs/index.vue
+++ b/amoro-web/src/views/catalogs/index.vue
@@ -26,6 +26,7 @@ import Detail from './Detail.vue'
 import { getCatalogList } from '@/services/table.service'
 import type { ICatalogItem } from '@/types/common.type'
 import { usePageScroll } from '@/hooks/usePageScroll'
+import { canManageCatalog } from '@/utils/permission'
 
 const { t } = useI18n()
 const router = useRouter()
@@ -39,6 +40,7 @@ const curCatalog = reactive<ICatalogItem>({
 const isEdit = ref<boolean>(false)
 const NEW_CATALOG = 'new catalog'
 const loading = ref<boolean>(false)
+const writable = ref<boolean>(canManageCatalog())
 const simpleImage = AEmpty.PRESENTED_IMAGE_SIMPLE
 
 async function getCatalogs() {
@@ -136,7 +138,7 @@ function addCatalog() {
     addNewCatalog()
   }
 }
-async function addNewCatalog() {
+  async function addNewCatalog() {
   const item: ICatalogItem = {
     catalogName: NEW_CATALOG,
     catalogType: '',
@@ -183,7 +185,7 @@ onBeforeRouteLeave((_to, _form, next) => {
             {{ item.catalogName }}
           </li>
         </ul>
-        <a-button :disabled="curCatalog.catalogName === NEW_CATALOG" 
class="add-btn" @click="addCatalog">
+        <a-button v-if="writable" :disabled="curCatalog.catalogName === 
NEW_CATALOG" class="add-btn" @click="addCatalog">
           +
         </a-button>
       </div>
diff --git a/amoro-web/src/views/hive-details/index.vue 
b/amoro-web/src/views/hive-details/index.vue
index 3e67c8c76..a23781ab1 100644
--- a/amoro-web/src/views/hive-details/index.vue
+++ b/amoro-web/src/views/hive-details/index.vue
@@ -25,6 +25,7 @@ import errorMsg from './components/ErrorMsg.vue'
 import type { DetailColumnItem } from '@/types/common.type'
 import { upgradeStatusMap } from '@/types/common.type'
 import { getHiveTableDetail, getUpgradeStatus } from '@/services/table.service'
+import { canManageTable } from '@/utils/permission'
 
 export default defineComponent({
   name: 'Tables',
@@ -42,6 +43,7 @@ export default defineComponent({
     const isSecondaryNav = computed(() => {
       return !!(route.path.includes('upgrade'))
     })
+    const writable = computed(() => canManageTable())
 
     const state = reactive({
       loading: false,
@@ -172,6 +174,7 @@ export default defineComponent({
       upgradeTable,
       goBack,
       refresh,
+      writable,
     }
   },
 })
@@ -183,7 +186,7 @@ export default defineComponent({
       <div class="g-flex-jsb table-top">
         <span :title="tableName" class="table-name g-text-nowrap">{{ tableName 
}}</span>
         <div class="right-btn">
-          <a-button type="primary" :disabled="status === 
upgradeStatus.upgrading" @click="upgradeTable">
+          <a-button v-if="writable" type="primary" :disabled="status === 
upgradeStatus.upgrading" @click="upgradeTable">
             {{ displayStatus }}
           </a-button>
           <p v-if="status === upgradeStatus.failed" class="fail-msg" 
@click="showErrorMsg = true">
diff --git a/amoro-web/src/views/login/index.vue 
b/amoro-web/src/views/login/index.vue
index fb897123e..cd1884823 100644
--- a/amoro-web/src/views/login/index.vue
+++ b/amoro-web/src/views/login/index.vue
@@ -22,6 +22,7 @@ import { defineComponent, reactive, ref, onMounted, 
onUnmounted, nextTick } from
 import { useRouter } from 'vue-router'
 import loginService from '@/services/login.service'
 import { usePlaceholder } from '@/hooks/usePlaceholder'
+import useStore from '@/store'
 
 interface FormState {
   username: string
@@ -32,6 +33,7 @@ export default defineComponent({
   name: 'Login',
   setup() {
     const router = useRouter()
+    const store = useStore()
     const formState = reactive<FormState>({
       username: '',
       password: '',
@@ -114,6 +116,15 @@ export default defineComponent({
           message.error(res.message)
           return
         }
+        const result = res.result || {}
+        const roles = result.roles || (result.role ? [result.role] : [])
+        const privileges = result.privileges || []
+        store.updateUserInfo({
+          userName: result.userName || formState.username,
+          role: roles[0],
+          roles,
+          privileges,
+        })
         setTimeout(() => {
           window.location.href = '/'
         }, 100)
diff --git a/amoro-web/src/views/resource/components/List.vue 
b/amoro-web/src/views/resource/components/List.vue
index 908c7193b..063c2db7c 100644
--- a/amoro-web/src/views/resource/components/List.vue
+++ b/amoro-web/src/views/resource/components/List.vue
@@ -24,6 +24,7 @@ import type { IIOptimizeGroupItem, IOptimizeResourceTableItem 
} from '@/types/co
 import { getOptimizerResourceList, getResourceGroupsListAPI, groupDeleteAPI, 
groupDeleteCheckAPI, releaseResource } from '@/services/optimize.service'
 import { usePagination } from '@/hooks/usePagination'
 import { dateFormat, mbToSize } from '@/utils'
+import { canManageOptimizer } from '@/utils/permission'
 
 const props = defineProps<{ curGroupName?: string, type: string }>()
 
@@ -47,6 +48,7 @@ const STATUS_CONFIG = shallowReactive({
 
 const loading = ref<boolean>(false)
 const releaseLoading = ref<boolean>(false)
+const writable = canManageOptimizer()
 const tableColumns = shallowReactive([
   { dataIndex: 'name', title: t('name'), ellipsis: true },
   { dataIndex: 'container', title: t('container'), width: '23%', ellipsis: 
true },
@@ -218,7 +220,7 @@ onMounted(() => {
           <span>{{ record.optimizeStatus }}</span>
         </template>
         <template v-if="column.dataIndex === 'operation'">
-          <span
+          <span v-if="writable"
             class="primary-link" :class="{ disabled: record.container === 
'external' }"
             @click="releaseModal(record)"
           >
@@ -226,12 +228,14 @@ onMounted(() => {
           </span>
         </template>
         <template v-if="column.dataIndex === 'operationGroup'">
+          <template v-if="writable">
           <span class="primary-link g-mr-12" @click="editGroup(record)">
             {{ t('edit') }}
           </span>
           <span class="primary-link" @click="removeGroup(record)">
             {{ t('remove') }}
           </span>
+          </template>
         </template>
       </template>
     </a-table>
diff --git a/amoro-web/src/views/resource/index.vue 
b/amoro-web/src/views/resource/index.vue
index a3fd96545..e3b742309 100644
--- a/amoro-web/src/views/resource/index.vue
+++ b/amoro-web/src/views/resource/index.vue
@@ -35,6 +35,7 @@ import type { IIOptimizeGroupItem, ILableAndValue } from 
'@/types/common.type'
 import GroupModal from '@/views/resource/components/GroupModal.vue'
 import CreateOptimizerModal from 
'@/views/resource/components/CreateOptimizerModal.vue'
 import { usePageScroll } from '@/hooks/usePageScroll'
+import { canManageOptimizer } from '@/utils/permission'
 
 export default defineComponent({
   name: 'Resource',
@@ -49,6 +50,7 @@ export default defineComponent({
     const router = useRouter()
     const route = useRoute()
     const { pageScrollRef } = usePageScroll()
+    const writable = canManageOptimizer()
     const tabConfig: ILableAndValue[] = shallowReactive([
       { label: t('optimizerGroups'), value: 'optimizerGroups' },
       { label: t('optimizers'), value: 'optimizers' },
@@ -131,6 +133,7 @@ export default defineComponent({
       createOptimizer,
       t,
       pageScrollRef,
+      writable,
     }
   },
 })
@@ -158,7 +161,7 @@ export default defineComponent({
               :tab="t('optimizers')"
               :class="[activeTab === 'optimizers' ? 'active' : '']"
             >
-              <a-button type="primary" class="g-mb-16" 
@click="createOptimizer(null)">
+              <a-button v-if="writable" type="primary" class="g-mb-16" 
@click="createOptimizer(null)">
                 {{ t("createOptimizer") }}
               </a-button>
               <List type="optimizers" />
@@ -168,7 +171,7 @@ export default defineComponent({
               :tab="t('optimizerGroups')"
               :class="[activeTab === 'optimizerGroups' ? 'active' : '']"
             >
-              <a-button type="primary" class="g-mb-16" 
@click="editGroup(null)">
+              <a-button v-if="writable" type="primary" class="g-mb-16" 
@click="editGroup(null)">
                 {{ t("addGroup") }}
               </a-button>
               <List
diff --git a/amoro-web/src/views/tables/components/Optimizing.vue 
b/amoro-web/src/views/tables/components/Optimizing.vue
index c56398c7b..c7ccc0b50 100644
--- a/amoro-web/src/views/tables/components/Optimizing.vue
+++ b/amoro-web/src/views/tables/components/Optimizing.vue
@@ -25,6 +25,7 @@ import { usePagination } from '@/hooks/usePagination'
 import type { BreadcrumbOptimizingItem, IColumns, ILableAndValue } from 
'@/types/common.type'
 import { cancelOptimizingProcess, getOptimizingProcesses, 
getTableOptimizingTypes, getTasksByOptimizingProcessId } from 
'@/services/table.service'
 import { bytesToSize, dateFormat, formatMS2Time } from '@/utils/index'
+import { canManageTable } from '@/utils/permission'
 
 const hasBreadcrumb = ref<boolean>(false)
 
@@ -77,6 +78,7 @@ const breadcrumbDataSource = 
reactive<BreadcrumbOptimizingItem[]>([])
 
 const loading = ref<boolean>(false)
 const cancelDisabled = ref(true)
+const writable = ref<boolean>(canManageTable())
 const pagination = reactive(usePagination())
 const breadcrumbPagination = reactive(usePagination())
 const route = useRoute()
@@ -328,7 +330,7 @@ onMounted(() => {
           </a-breadcrumb>
         </a-col>
         <a-col :span="6">
-          <a-button
+          <a-button v-if="writable"
             v-model:disabled="cancelDisabled" type="primary" class="g-mb-16" 
style="float: right"
             @click="cancel"
           >
diff --git a/amoro-web/src/views/terminal/index.vue 
b/amoro-web/src/views/terminal/index.vue
index 4e696c56a..5fa4074b8 100644
--- a/amoro-web/src/views/terminal/index.vue
+++ b/amoro-web/src/views/terminal/index.vue
@@ -28,6 +28,7 @@ import { executeSql, getExampleSqlCode, getJobDebugResult, 
getLastDebugInfo, get
 import { getCatalogList } from '@/services/table.service'
 import { usePlaceholder } from '@/hooks/usePlaceholder'
 import { usePageScroll } from '@/hooks/usePageScroll'
+import { canExecuteSql } from '@/utils/permission'
 
 interface ISessionInfo {
   sessionId: string
@@ -53,6 +54,7 @@ export default defineComponent({
     const sqlLogRef = ref<any>(null)
     const { pageScrollRef } = usePageScroll()
     const readOnly = ref<boolean>(false)
+    const writable = ref<boolean>(canExecuteSql())
     const sqlSource = ref<string>('')
     const showDebug = ref<boolean>(false)
     const runStatus = ref<string>('')
@@ -130,6 +132,10 @@ export default defineComponent({
     }
 
     async function handleDebug() {
+      if (!writable.value) {
+        message.error('ReadOnly user cannot execute SQL')
+        return
+      }
       try {
         if (!curCatalog.value) {
           message.error(placeholder.selectClPh)
@@ -334,6 +340,7 @@ export default defineComponent({
       handleFull,
       resultFull,
       showDebug,
+      writable,
       sqlSource,
       readOnly,
       generateCode,
@@ -364,13 +371,13 @@ export default defineComponent({
                 </div>
                 <a-tooltip v-if="runStatus === 'Running'" :title="$t('pause')" 
placement="bottom">
                   <svg-icon
-                    class-name="icon-svg" icon-class="sqlpause" 
class="g-mr-12" :disabled="readOnly"
+                    class-name="icon-svg" icon-class="sqlpause" 
class="g-mr-12" :disabled="readOnly || !writable"
                     @click="handleIconClick('pause')"
                   />
                 </a-tooltip>
                 <a-tooltip v-else :title="$t('run')" placement="bottom">
                   <svg-icon
-                    class-name="icon-svg" icon-class="sqldebug" 
class="g-mr-12" :disabled="readOnly"
+                    class-name="icon-svg" icon-class="sqldebug" 
class="g-mr-12" :disabled="readOnly || !writable"
                     @click="handleIconClick('debug')"
                   />
                 </a-tooltip>
@@ -411,7 +418,7 @@ export default defineComponent({
             </div>
             <a-button
               v-for="code in shortcuts" :key="code" type="link"
-              :disabled="runStatus === 'Running' || runStatus === 'Canceling'" 
class="code" @click="generateCode(code)"
+              :disabled="runStatus === 'Running' || runStatus === 'Canceling' 
|| !writable" class="code" @click="generateCode(code)"
             >
               {{
                 code
diff --git a/dev/deps/dependencies-hadoop-2-spark-3.3 
b/dev/deps/dependencies-hadoop-2-spark-3.3
index cd86dfc6f..d8a476f6e 100644
--- a/dev/deps/dependencies-hadoop-2-spark-3.3
+++ b/dev/deps/dependencies-hadoop-2-spark-3.3
@@ -28,6 +28,7 @@ arrow-vector/15.0.2//arrow-vector-15.0.2.jar
 asm/5.0.4//asm-5.0.4.jar
 async-profiler/2.9//async-profiler-2.9.jar
 auth/2.24.12//auth-2.24.12.jar
+aviator/5.9.0//aviator-5.9.0.jar
 avro-ipc/1.11.0//avro-ipc-1.11.0.jar
 avro-mapred/1.11.0//avro-mapred-1.11.0.jar
 avro/1.11.3//avro-1.11.3.jar
@@ -53,11 +54,12 @@ commons-compiler/3.0.16//commons-compiler-3.0.16.jar
 commons-compress/1.23.0//commons-compress-1.23.0.jar
 commons-configuration/1.6//commons-configuration-1.6.jar
 commons-crypto/1.1.0//commons-crypto-1.1.0.jar
+commons-csv/1.12.0//commons-csv-1.12.0.jar
 commons-dbcp/1.4//commons-dbcp-1.4.jar
 commons-dbcp2/2.9.0//commons-dbcp2-2.9.0.jar
 commons-digester/1.8//commons-digester-1.8.jar
 commons-el/1.0//commons-el-1.0.jar
-commons-io/2.11.0//commons-io-2.11.0.jar
+commons-io/2.20.0//commons-io-2.20.0.jar
 commons-lang/2.6//commons-lang-2.6.jar
 commons-lang3/3.20.0//commons-lang3-3.20.0.jar
 commons-logging/1.2//commons-logging-1.2.jar
@@ -105,7 +107,7 @@ 
flink-shaded-netty/4.1.91.Final-17.0//flink-shaded-netty-4.1.91.Final-17.0.jar
 flink-streaming-java/1.20.3//flink-streaming-java-1.20.3.jar
 geronimo-jcache_1.0_spec/1.0-alpha-1//geronimo-jcache_1.0_spec-1.0-alpha-1.jar
 glue/2.24.12//glue-2.24.12.jar
-gson/2.8.6//gson-2.8.6.jar
+gson/2.11.0//gson-2.11.0.jar
 guava/32.1.1-jre//guava-32.1.1-jre.jar
 hadoop-annotations/2.10.2//hadoop-annotations-2.10.2.jar
 hadoop-auth/2.10.2//hadoop-auth-2.10.2.jar
@@ -162,6 +164,7 @@ 
iceberg-spark-3.3_2.12/1.6.1//iceberg-spark-3.3_2.12-1.6.1.jar
 
iceberg-spark-extensions-3.3_2.12/1.6.1//iceberg-spark-extensions-3.3_2.12-1.6.1.jar
 icu4j/69.1//icu4j-69.1.jar
 identity-spi/2.24.12//identity-spi-2.24.12.jar
+ipaddress/5.5.1//ipaddress-5.5.1.jar
 ivy/2.5.1//ivy-2.5.1.jar
 j2objc-annotations/2.8//j2objc-annotations-2.8.jar
 jackson-annotations/2.14.2//jackson-annotations-2.14.2.jar
@@ -192,6 +195,7 @@ javolution/5.5.1//javolution-5.5.1.jar
 jaxb-api/2.2.2//jaxb-api-2.2.2.jar
 jboss-logging/3.3.1.Final//jboss-logging-3.3.1.Final.jar
 jboss-threads/2.3.6.Final//jboss-threads-2.3.6.Final.jar
+jcasbin/1.99.0//jcasbin-1.99.0.jar
 jcip-annotations/1.0-1//jcip-annotations-1.0-1.jar
 jdo-api/3.0.1//jdo-api-3.0.1.jar
 jersey-client/2.36//jersey-client-2.36.jar
diff --git a/dev/deps/dependencies-hadoop-3-spark-3.5 
b/dev/deps/dependencies-hadoop-3-spark-3.5
index 2facef79f..59f2a1c2b 100644
--- a/dev/deps/dependencies-hadoop-3-spark-3.5
+++ b/dev/deps/dependencies-hadoop-3-spark-3.5
@@ -23,6 +23,7 @@ arrow-vector/15.0.2//arrow-vector-15.0.2.jar
 asm/5.0.4//asm-5.0.4.jar
 async-profiler/2.9//async-profiler-2.9.jar
 auth/2.24.12//auth-2.24.12.jar
+aviator/5.9.0//aviator-5.9.0.jar
 avro-ipc/1.11.4//avro-ipc-1.11.4.jar
 avro-mapred/1.11.4//avro-mapred-1.11.4.jar
 avro/1.11.3//avro-1.11.3.jar
@@ -47,9 +48,10 @@ commons-collections4/4.4//commons-collections4-4.4.jar
 commons-compiler/3.1.9//commons-compiler-3.1.9.jar
 commons-compress/1.23.0//commons-compress-1.23.0.jar
 commons-crypto/1.1.0//commons-crypto-1.1.0.jar
+commons-csv/1.12.0//commons-csv-1.12.0.jar
 commons-dbcp/1.4//commons-dbcp-1.4.jar
 commons-dbcp2/2.9.0//commons-dbcp2-2.9.0.jar
-commons-io/2.16.1//commons-io-2.16.1.jar
+commons-io/2.20.0//commons-io-2.20.0.jar
 commons-lang/2.6//commons-lang-2.6.jar
 commons-lang3/3.20.0//commons-lang3-3.20.0.jar
 commons-logging/1.2//commons-logging-1.2.jar
@@ -98,7 +100,7 @@ 
flink-shaded-jackson/2.14.2-17.0//flink-shaded-jackson-2.14.2-17.0.jar
 flink-shaded-netty/4.1.91.Final-17.0//flink-shaded-netty-4.1.91.Final-17.0.jar
 flink-streaming-java/1.20.3//flink-streaming-java-1.20.3.jar
 glue/2.24.12//glue-2.24.12.jar
-gson/2.10.1//gson-2.10.1.jar
+gson/2.11.0//gson-2.11.0.jar
 guava/32.1.1-jre//guava-32.1.1-jre.jar
 hadoop-aws/3.4.2//hadoop-aws-3.4.2.jar
 hadoop-client-api/3.4.2//hadoop-client-api-3.4.2.jar
@@ -145,6 +147,7 @@ 
iceberg-spark-3.5_2.12/1.6.1//iceberg-spark-3.5_2.12-1.6.1.jar
 
iceberg-spark-extensions-3.5_2.12/1.6.1//iceberg-spark-extensions-3.5_2.12-1.6.1.jar
 icu4j/69.1//icu4j-69.1.jar
 identity-spi/2.24.12//identity-spi-2.24.12.jar
+ipaddress/5.5.1//ipaddress-5.5.1.jar
 ivy/2.5.1//ivy-2.5.1.jar
 j2objc-annotations/2.8//j2objc-annotations-2.8.jar
 jackson-annotations/2.14.2//jackson-annotations-2.14.2.jar
@@ -168,6 +171,7 @@ javax.servlet-api/3.1.0//javax.servlet-api-3.1.0.jar
 javolution/5.5.1//javolution-5.5.1.jar
 jboss-logging/3.3.1.Final//jboss-logging-3.3.1.Final.jar
 jboss-threads/2.3.6.Final//jboss-threads-2.3.6.Final.jar
+jcasbin/1.99.0//jcasbin-1.99.0.jar
 jdo-api/3.0.1//jdo-api-3.0.1.jar
 jersey-client/2.40//jersey-client-2.40.jar
 jersey-common/2.40//jersey-common-2.40.jar
diff --git a/dist/src/main/amoro-bin/conf/config.yaml 
b/dist/src/main/amoro-bin/conf/config.yaml
index 884ce5896..27d3e5eb4 100644
--- a/dist/src/main/amoro-bin/conf/config.yaml
+++ b/dist/src/main/amoro-bin/conf/config.yaml
@@ -40,6 +40,36 @@ ams:
     # login-auth-provider: 
org.apache.amoro.server.authentication.LdapPasswdAuthenticationProvider
     # login-auth-ldap-url: "ldap://ldap.example.com:389";
     # login-auth-ldap-user-pattern: "uid={0},ou=people,dc=example,dc=com"
+    #
+    # ── Authorization (RBAC) ──
+    # Authorization enablement example:
+    # authorization:
+    #   enabled: true
+    #
+    # LDAP Group role-mapping example:
+    # authorization:
+    #   enabled: true
+    #   ldap-role-mapping:
+    #     enabled: true
+    #     group-member-attribute: "member"
+    #     user-dn-pattern: "uid={0},ou=people,dc=example,dc=com"
+    #     bind-dn: "cn=service-account,dc=example,dc=com"
+    #     bind-password: "service-password"
+    #     groups:
+    #       - group-dn: "cn=amoro-service-admins,ou=groups,dc=example,dc=com"
+    #         role: SERVICE_ADMIN
+    #       - group-dn: "cn=amoro-viewers,ou=groups,dc=example,dc=com"
+    #         role: VIEWER
+    #       - group-dn: "cn=amoro-catalog-admins,ou=groups,dc=example,dc=com"
+    #         role: CATALOG_ADMIN
+    #
+    # Custom roles are string-based. To use a custom role such as 
CATALOG_ADMIN,
+    # define its privileges in the built-in Casbin policy resource and map an
+    # LDAP group to that role name here.
+    #
+    # Optional fallback:
+    #   set default-role: VIEWER only when you intentionally want authenticated
+    #   users without a matched role mapping to receive read-only access.
 
   refresh-external-catalogs:
     interval: 3min # 180000
@@ -111,7 +141,7 @@ ams:
   # Support for encrypted sensitive configuration items
   shade:
     identifier: default # Built-in support for default/base64. Defaults to 
"default", indicating no encryption
-    sensitive-keywords: admin-password;database.password
+    sensitive-keywords: 
admin-password;database.password;http-server.authorization.ldap-role-mapping.bind-password
 
   overview-cache:
     refresh-interval: 3min          # 3 min
diff --git a/docs/configuration/ams-config.md b/docs/configuration/ams-config.md
index ec4f34c39..c929ef2a8 100644
--- a/docs/configuration/ams-config.md
+++ b/docs/configuration/ams-config.md
@@ -87,6 +87,14 @@ table td:last-child, table th:last-child { width: 40%; 
word-break: break-all; }
 | ha.zookeeper-auth-type | NONE | The Zookeeper authentication type, NONE or 
KERBEROS. |
 | http-server.auth-basic-provider | 
org.apache.amoro.server.authentication.DefaultPasswdAuthenticationProvider | 
User-defined password authentication implementation of 
org.apache.amoro.authentication.PasswdAuthenticationProvider |
 | http-server.auth-jwt-provider | &lt;undefined&gt; | User-defined JWT (JSON 
Web Token) authentication implementation of 
org.apache.amoro.authentication.TokenAuthenticationProvider |
+| http-server.authorization.default-role | &lt;undefined&gt; | Optional 
default dashboard role for authenticated users without an LDAP role mapping. |
+| http-server.authorization.enabled | false | Whether to enable dashboard RBAC 
authorization. |
+| http-server.authorization.ldap-role-mapping.bind-dn |  | Optional LDAP bind 
DN used when querying role-mapping groups. |
+| http-server.authorization.ldap-role-mapping.bind-password |  | Optional LDAP 
bind password used when querying role-mapping groups. |
+| http-server.authorization.ldap-role-mapping.enabled | false | Whether to 
resolve dashboard roles from LDAP group membership. |
+| http-server.authorization.ldap-role-mapping.group-member-attribute | member 
| LDAP group attribute that stores member references. |
+| http-server.authorization.ldap-role-mapping.groups | &lt;undefined&gt; | 
LDAP group-to-role mapping entries containing group-dn and role fields. |
+| http-server.authorization.ldap-role-mapping.user-dn-pattern | 
&lt;undefined&gt; | LDAP user DN pattern used to match group members. Use {0} 
as the username placeholder. |
 | http-server.bind-port | 19090 | Port that the Http server is bound to. |
 | http-server.login-auth-ldap-url | &lt;undefined&gt; | LDAP connection 
URL(s), value could be a SPACE separated list of URLs to multiple LDAP servers 
for resiliency. URLs are tried in the order specified until the connection is 
successful |
 | http-server.login-auth-ldap-user-pattern | &lt;undefined&gt; | LDAP user 
pattern for authentication. The pattern defines how to construct the user's 
distinguished name (DN) in the LDAP directory. Use {0} as a placeholder for the 
username. For example, 'cn={0},ou=people,dc=example,dc=com' will search for 
users in the specified organizational unit. |
@@ -135,6 +143,91 @@ table td:last-child, table th:last-child { width: 40%; 
word-break: break-all; }
 | thrift-server.table-service.worker-thread-count | 20 | The number of worker 
threads for the Thrift server. |
 | use-master-slave-mode | false | This setting controls whether to enable the 
AMS horizontal scaling feature, which is currently under development and 
testing. |
 
+## RBAC Example
+
+Enable RBAC only when you need role separation for dashboard users.
+
+The current RBAC model uses:
+
+- string-based roles
+- LDAP group-to-role mapping as the primary role source
+- built-in Casbin policy to translate roles into privileges
+- privilege-driven frontend authorization
+
+Amoro provides two built-in roles by default:
+
+| Role | Description | Default Privileges |
+| --- | --- | --- |
+| `SERVICE_ADMIN` | Platform administrator | All privileges |
+| `VIEWER` | Read-only resource viewer | `VIEW_CATALOG`, `VIEW_TABLE`, 
`VIEW_OPTIMIZER` |
+
+`VIEWER` does not include `VIEW_SYSTEM`, so it cannot access `Overview` or 
`Terminal`.
+After login succeeds, `/login/current` returns both `roles` and effective 
`privileges`.
+
+If you need additional roles, define them by Casbin policy and map LDAP groups 
to those
+role names. The role name itself does not need to be added to Java enum code.
+
+```yaml
+ams:
+  http-server:
+    authorization:
+      enabled: true
+```
+
+```yaml
+ams:
+  http-server:
+    login-auth-provider: 
org.apache.amoro.server.authentication.LdapPasswdAuthenticationProvider
+    login-auth-ldap-url: "ldap://ldap.example.com:389";
+    login-auth-ldap-user-pattern: "uid={0},ou=people,dc=example,dc=com"
+    authorization:
+      enabled: true
+      ldap-role-mapping:
+        enabled: true
+        group-member-attribute: "member"
+        user-dn-pattern: "uid={0},ou=people,dc=example,dc=com"
+        bind-dn: "cn=service-account,dc=example,dc=com"
+        bind-password: "service-password"
+        groups:
+          - group-dn: "cn=amoro-service-admins,ou=groups,dc=example,dc=com"
+            role: SERVICE_ADMIN
+          - group-dn: "cn=amoro-viewers,ou=groups,dc=example,dc=com"
+            role: VIEWER
+          - group-dn: "cn=amoro-catalog-admins,ou=groups,dc=example,dc=com"
+            role: CATALOG_ADMIN
+```
+
+Example `/login/current` response:
+
+```json
+{
+  "userName": "alice",
+  "roles": ["CATALOG_ADMIN"],
+  "privileges": [
+    "VIEW_CATALOG",
+    "MANAGE_CATALOG",
+    "VIEW_TABLE",
+    "MANAGE_TABLE"
+  ]
+}
+```
+
+Example custom role policy:
+
+```csv
+p, CATALOG_ADMIN, CATALOG, GLOBAL, VIEW_CATALOG, allow
+p, CATALOG_ADMIN, CATALOG, GLOBAL, MANAGE_CATALOG, allow
+p, CATALOG_ADMIN, TABLE, GLOBAL, VIEW_TABLE, allow
+p, CATALOG_ADMIN, TABLE, GLOBAL, MANAGE_TABLE, allow
+```
+
+Notes:
+
+- Recommended production setup is explicit role assignment only.
+- `default-role` is optional. If it is not set, users who do not match any 
role mapping get no business role.
+- Use `default-role: VIEWER` only if you intentionally want authenticated 
users without a matched role mapping to receive read-only access.
+- Casbin model and default policy are built into the service and loaded from 
classpath.
+- Dashboard request-to-privilege mapping is also built into the service and 
loaded from a resource configuration file.
 
 ## Shade Utils Configuration
 
diff --git a/pom.xml b/pom.xml
index 070c9c0f1..506b88753 100644
--- a/pom.xml
+++ b/pom.xml
@@ -164,6 +164,7 @@
         <hudi.version>0.14.1</hudi.version>
         <pagehelper.version>6.1.0</pagehelper.version>
         <jsqlparser.version>4.7</jsqlparser.version>
+        <jcasbin.version>1.99.0</jcasbin.version>
         <fasterxml.jackson.version>2.14.2</fasterxml.jackson.version>
         
<apache-directory-server.version>2.0.0-M15</apache-directory-server.version>
         
<apache-directory-api-all.version>1.0.0-M20</apache-directory-api-all.version>

Reply via email to