zhoujinsong commented on code in PR #4118:
URL: https://github.com/apache/amoro/pull/4118#discussion_r2993236134


##########
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,

Review Comment:
   **[Bug] `resolvePrivileges` and `authorize` use inconsistent privilege 
sources**
   
   `resolvePrivileges` reads from the pre-built `rolePrivileges` map (loaded 
from the CSV), while `authorize` uses the live Casbin enforcer. If a policy row 
carries `eft=deny`, `authorize` will correctly block the request, but 
`resolvePrivileges` will still include that privilege in the list returned to 
the frontend (because `loadRolePrivileges` only filters rows whose index-5 
token is literally `"deny"`).
   
   Consider deriving privileges from the enforcer directly, e.g. 
`enforcer.getPermissionsForUser(role)`, so both paths stay in sync.



##########
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;
+    }

Review Comment:
   **[Bug] LDAP group query fails hard on every login when one group is 
unreachable**
   
   If any single group DN throws a `NamingException`, the resolver re-throws a 
`RuntimeException`, which propagates all the way to `LoginController` and 
causes the login to fail — even though authentication already succeeded and the 
user might belong to other groups.
   
   Consider: (1) catching the exception per-group, logging a warning, and 
continuing to query the remaining groups; (2) or at least documenting that a 
partial LDAP outage will break all logins. The current test 
`testThrowsWhenLookupFails` validates the hard-fail behaviour, but that policy 
seems too strict for a background enrichment step.



##########
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");

Review Comment:
   **[Bug] Path-prefix matching can produce false positives**
   
   `prefixes.stream().anyMatch(path::startsWith)` will match 
`/api/ams/v1/catalogs-extra` against the prefix `/api/ams/v1/catalogs`, 
granting unintended privileges to paths that merely share a common string 
prefix.
   
   Add a separator boundary check: `path.startsWith(prefix + "/") || 
path.equals(prefix)`.



##########
amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java:
##########
@@ -408,8 +419,30 @@ public void preHandleRequest(Context ctx) {
     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()
+                                + " "

Review Comment:
   **[Bug] Unmapped request paths return 403 instead of 404**
   
   When `privilegeMapper.resolve(ctx)` returns `Optional.empty()` (i.e., the 
request path is not covered by `privilege_mapping.yaml`), the code throws 
`ForbiddenException` with the message `"No authorization mapping for request: 
..."`. This means any new or typo-ed API endpoint silently returns 403 rather 
than 404, which is confusing to callers and leaks information about the 
authorization layer.
   
   A missing mapping means the resource doesn't exist or wasn't declared, not 
that the user lacks permission. Consider returning 404 for unmapped paths, or 
fall back to allowing the request (to avoid breaking undeclared internal 
endpoints).



##########
amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/LoginController.java:
##########
@@ -59,15 +73,37 @@ public void login(Context ctx) {
     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();

Review Comment:
   **[Minor] LDAP group lookup happens on every login — consider caching**
   
   `roleResolver.resolve(authenticatedUser)` queries all configured LDAP groups 
on every single login. For environments with many groups or a slow LDAP server 
this adds noticeable latency to the login flow. A short-lived cache (e.g. Guava 
`LoadingCache` with a 5-minute TTL) keyed by username would avoid repeated 
round trips without meaningfully delaying role updates.



##########
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)) {

Review Comment:
   **[Minor] Admin username comparison is case-sensitive**
   
   `adminUsername.equals(username)` — if 
`LdapPasswdAuthenticationProvider.normalizeUsername` strips the domain but the 
configured `admin-username` was set to `Admin` (capital A), the bootstrap admin 
will not get `SERVICE_ADMIN` role. Consider using `equalsIgnoreCase` or at 
least documenting the case-sensitivity requirement.



##########
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

Review Comment:
   **[Bug] `MANAGE_PLATFORM` mapping has no `methods` filter**
   
   The last mapping entry for `/api/ams/v1/files`, `/api/ams/v1/settings`, and 
`/api/ams/v1/api/token` omits the `methods` field. Per 
`DashboardPrivilegeMappingRule`, an empty method set means _all_ HTTP methods 
match. This causes GET requests to `/api/ams/v1/settings` to require 
`MANAGE_PLATFORM`, which is too strict — a read request should only need 
`VIEW_SYSTEM` or similar. Add explicit `methods` to distinguish read vs. write 
access.



##########
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[]
+}

Review Comment:
   **[Minor] `hasPrivilege` returns `true` for all paths when authorization is 
disabled**
   
   When RBAC is disabled the backend sets every user's privileges to the full 
`Privilege` enum set, so `hasPrivilege` will always return `true`. This is 
correct, but it means the frontend permission model silently relies on backend 
behaviour. If a future refactor changes the disabled-mode response, all 
frontend guards will break without a clear indication why. Consider adding a 
comment here explaining the contract: _"When authorization is disabled, the 
backend returns all privileges."_



##########
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({

Review Comment:
   **[Minor] 403 "no permission" errors are silently swallowed**
   
   When a 403 response does _not_ contain the word `"login"` in the message, 
the code rejects the promise but the caller's `handleError` branch is never 
invoked (because the early return skips the error-display logic). This means 
write operations forbidden by RBAC will silently fail from the user's 
perspective.
   
   Suggest surfacing the `No permission` message to the user via 
`message.error(...)` before rejecting.



##########
amoro-web/src/views/catalogs/index.vue:
##########
@@ -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())

Review Comment:
   **[Minor] `writable` is a non-reactive plain `ref`, not a `computed`**
   
   `const writable = ref<boolean>(canManageCatalog())` captures the privilege 
value at component mount time. If the store's `privileges` array is updated 
after mount (e.g., after a re-login within the same SPA session), `writable` 
will not update.
   
   `Detail.vue` does this correctly with `computed(() => canManageCatalog())`. 
Please align `catalogs/index.vue` to use `computed` as well.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to