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

imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git

commit dad5f45d9691efbc34856992a9c0981bc6663d3d
Author: dark <[email protected]>
AuthorDate: Sun Aug 16 03:48:50 2026 +0800

    feat(hubble): unify compatibility and auth modes
    
    - centralize server capability profiles in the client
    - derive authenticated and anonymous modes from the backend
    - expose readable GraphSpace permission presets
---
 README.md                                          |  13 +-
 .../org/apache/hugegraph/driver/HugeClient.java    |   3 +-
 .../hugegraph/driver/ServerCompatibility.java      |  67 ++++
 .../hugegraph/driver/ServerCompatibilityTest.java  |  41 ++
 hugegraph-hubble/AGENTS.md                         |  39 ++
 hugegraph-hubble/README.md                         |  37 ++
 .../docs/images/hubble-auth-connection-root.png    | Bin 0 -> 56400 bytes
 .../docs/images/hubble-auth-connection.png         | Bin 0 -> 41540 bytes
 .../docs/images/hubble-graphspace-permissions.png  | Bin 0 -> 60284 bytes
 .../docs/images/hubble-legacy-capability.png       | Bin 0 -> 45865 bytes
 hugegraph-hubble/docs/images/hubble-non-auth.png   | Bin 0 -> 41540 bytes
 .../docs/images/hubble-pd-connection.png           | Bin 0 -> 47441 bytes
 .../org/apache/hugegraph/config/WebMvcConfig.java  |   1 +
 .../hugegraph/controller/BaseController.java       |  12 +-
 .../hugegraph/controller/ConfigController.java     |   1 +
 .../hugegraph/controller/auth/LoginController.java |   4 +
 .../apache/hugegraph/entity/auth/UserEntity.java   |   7 +
 .../hugegraph/handler/CustomInterceptor.java       |  34 +-
 .../apache/hugegraph/handler/LoginInterceptor.java |   8 +
 .../apache/hugegraph/options/HubbleOptions.java    |   9 +
 .../hugegraph/service/auth/AuthContextService.java |  25 ++
 .../hugegraph/service/auth/AuthModeService.java    |  47 +++
 .../service/auth/GraphSpaceUserService.java        |  90 +++++
 .../apache/hugegraph/service/auth/UserService.java |  89 +++++
 hugegraph-hubble/hubble-fe/src/App.js              |  40 ++
 hugegraph-hubble/hubble-fe/src/auth/AuthContext.js |  10 +-
 .../hubble-fe/src/components/Topbar/index.ant.js   |  39 +-
 .../src/i18n/resources/en-US/modules/pages.json    |  15 +-
 .../src/i18n/resources/zh-CN/modules/pages.json    |  15 +-
 .../hubble-fe/src/pages/Account/EditLayer.js       |  96 +++--
 .../hubble-fe/src/pages/Account/SpaceAccess.js     | 439 +++------------------
 .../src/pages/Account/SpaceAccess.test.js          |  61 ++-
 .../pages/Account/account-edit-recovery.test.js    |   6 +-
 .../src/pages/Account/account-recovery.test.js     |   9 +-
 .../hubble-fe/src/pages/Account/index.js           |  10 +-
 .../src/pages/Account/permissionPresets.js         |  79 ++++
 .../src/pages/Account/permissionPresets.test.js    |  70 ++++
 hugegraph-hubble/hubble-fe/src/routes/index.js     |   8 +-
 hugegraph-hubble/hubble-fe/src/utils/config.js     |   6 +-
 39 files changed, 957 insertions(+), 473 deletions(-)

diff --git a/README.md b/README.md
index 02c1bb719..3eb7a6253 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,17 @@
 
 A comprehensive suite of client SDKs, data tools, and management utilities for 
[Apache HugeGraph](https://github.com/apache/hugegraph) graph database. Build 
applications, load data, and manage graphs with production-ready tools.
 
+Hubble's primary authentication and connection design targets HugeGraph
+`1.8/master`: PD discovery supplies the server address, anonymous mode uses a
+real unauthenticated client, and account/GraphSpace permissions are reduced to
+four readable presets. A thin adapter keeps 1.7 usable and limits 1.5 to its
+standalone core graph workflow; version checks are centralized rather than
+spread across UI pages.
+
+![Hubble authentication and connection 
modes](hugegraph-hubble/docs/images/hubble-auth-connection.png)
+
+![Hubble GraphSpace permission 
presets](hugegraph-hubble/docs/images/hubble-graphspace-permissions.png)
+
 **Quick Navigation**: [Architecture](#architecture-overview) | [Quick 
Start](#quick-start) | [Modules](#module-overview) | 
[Build](#build--development) | [Docker](#docker) | [Related 
Projects](#related-projects)
 
 ## Related Projects
@@ -57,7 +68,7 @@ graph TB
     CLIENT --> HUBBLE
     CLIENT --> TOOLS
     CLIENT --> SPARK
-    HUBBLE -.->|WIP: pd-client| PD
+    HUBBLE -.->|PD discovery UI| PD
 
     LOADER -.->|Sources| SRC["CSV | JSON | HDFS<br/>MySQL | Kafka"]
     SPARK -.->|I/O| SPK["Spark DataFrames"]
diff --git 
a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java 
b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java
index 3f26091f3..f02fd6146 100644
--- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java
+++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java
@@ -209,7 +209,8 @@ public class HugeClient implements Closeable {
         //       0.81 equals to the {latest_api_version} +10
         VersionUtil.check(apiVersion, "0.38", "0.81", "hugegraph-api in 
server");
         this.client.apiVersion(apiVersion);
-        boolean supportGs = VersionUtil.gte(this.version.getCoreVersion(), 
"1.7.0");
+        boolean supportGs = ServerCompatibility.supportsGraphSpace(
+                this.version.getCoreVersion());
         this.client.setSupportGs(supportGs);
     }
 
diff --git 
a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java
 
b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java
new file mode 100644
index 000000000..ac975d661
--- /dev/null
+++ 
b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hugegraph.driver;
+
+import org.apache.hugegraph.util.VersionUtil;
+
+/**
+ * Small compatibility boundary shared by Hubble and clients.
+ *
+ * <p>Version checks belong here so callers can express capabilities instead
+ * of branching on server versions in controllers or pages. Unknown versions
+ * deliberately use the conservative legacy profile.</p>
+ */
+public final class ServerCompatibility {
+
+    private static final String GRAPHSPACE_MIN_VERSION = "1.7.0";
+
+    private ServerCompatibility() {
+    }
+
+    public static Profile profile(String coreVersion) {
+        if (coreVersion == null || coreVersion.trim().isEmpty()) {
+            return Profile.LEGACY;
+        }
+        try {
+            String normalized = coreVersion.trim();
+            return VersionUtil.gte(normalized, GRAPHSPACE_MIN_VERSION) ?
+                   Profile.MODERN : Profile.LEGACY;
+        } catch (RuntimeException ignored) {
+            return Profile.LEGACY;
+        }
+    }
+
+    public static boolean supportsGraphSpace(String coreVersion) {
+        return profile(coreVersion).supportsGraphSpace();
+    }
+
+    public enum Profile {
+        LEGACY(false),
+        MODERN(true);
+
+        private final boolean graphSpace;
+
+        Profile(boolean graphSpace) {
+            this.graphSpace = graphSpace;
+        }
+
+        public boolean supportsGraphSpace() {
+            return this.graphSpace;
+        }
+    }
+}
diff --git 
a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java
 
b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java
new file mode 100644
index 000000000..f3bccbf17
--- /dev/null
+++ 
b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.hugegraph.driver;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ServerCompatibilityTest {
+
+    @Test
+    public void shouldKeepLegacyServersConservative() {
+        Assert.assertFalse(ServerCompatibility.supportsGraphSpace("1.5.0"));
+        Assert.assertFalse(ServerCompatibility.supportsGraphSpace("1.6.0"));
+        Assert.assertFalse(ServerCompatibility.supportsGraphSpace(null));
+        
Assert.assertFalse(ServerCompatibility.supportsGraphSpace("not-a-version"));
+    }
+
+    @Test
+    public void shouldExposeGraphSpaceForModernServers() {
+        Assert.assertTrue(ServerCompatibility.supportsGraphSpace("1.7.0"));
+        Assert.assertTrue(ServerCompatibility.supportsGraphSpace(" 1.7.0 "));
+        Assert.assertTrue(ServerCompatibility.supportsGraphSpace("1.8.0"));
+        Assert.assertEquals(ServerCompatibility.Profile.MODERN,
+                            ServerCompatibility.profile("1.7.1"));
+    }
+}
diff --git a/hugegraph-hubble/AGENTS.md b/hugegraph-hubble/AGENTS.md
new file mode 100644
index 000000000..9f732a344
--- /dev/null
+++ b/hugegraph-hubble/AGENTS.md
@@ -0,0 +1,39 @@
+# Hubble contributor guide
+
+## Authentication and connection boundary
+
+The `1.8/master` path is the source of truth. Backend configuration exposes
+one `auth.enabled` switch and one connection resolver. The resolver chooses
+either a direct server URL or an address discovered from PD; callers must not
+reimplement `usePD` or infer connection state from page-local flags. In PD mode
+the server address returned by discovery is authoritative, so a manual server
+URL is not required.
+
+Use the unauthenticated HugeGraph client for anonymous mode. Do not manufacture
+an empty token or an administrator session. Anonymous mode has no account
+context and account/permission routes are hidden or rejected at the capability
+boundary.
+
+## Compatibility policy
+
+Compatibility is intentionally one-way:
+
+- `1.8/master`: modern GraphSpace/auth contracts and the complete UI.
+- `1.7`: thin fallback for the legacy response shape; keep the core workflow
+  usable without adding version branches to controllers or React pages.
+- `1.5` standalone: core graph/schema/data operations only. GraphSpace
+  management is unsupported and should degrade with an explicit capability
+  response. Do not add a PD variant for 1.5.
+
+Version checks belong in the client compatibility adapter and connection
+resolver. New code should consume capabilities, not compare literal versions.
+When an old image cannot satisfy a capability, mark the test as `needs input`
+or `skipped` with the exact image tag and reason.
+
+## Verification
+
+For UI changes, use Chrome to exercise login/non-auth mode, connection
+switching, and account/GraphSpace visibility. Static inspection and unit tests
+are not a substitute for this interaction check. Keep screenshots collected
+from the running UI in the documentation assets referenced by
+`README.md`.
diff --git a/hugegraph-hubble/README.md b/hugegraph-hubble/README.md
index cbc13ad04..5b3fb9955 100644
--- a/hugegraph-hubble/README.md
+++ b/hugegraph-hubble/README.md
@@ -7,6 +7,43 @@
 hugegraph-hubble is a graph management and analysis platform that provides 
features:
 graph data load, schema management, graph relationship analysis, and graphical 
display.
 
+## Authentication, connections, and compatibility
+
+Hubble uses one capability-driven connection boundary for `1.8/master`.
+`auth.enabled=true` creates an authenticated session; when it is `false`, 
Hubble
+uses an unauthenticated client and does not create a fake user. Account and
+permission entry points are hidden in anonymous mode. Connection switching
+always goes through the backend resolver. In PD mode, a valid server address
+returned by discovery is sufficient; a manually configured server URL is not
+required.
+
+The UI presents four stable permission meanings: super administrator, 
GraphSpace
+read-only, GraphSpace read-write, and GraphSpace administrator. The last one
+means member management plus read/write within that GraphSpace; low-level
+`role`, `target`, `access`, and `belong` fields are not exposed.
+
+The compatibility boundary is deliberately small. Server 1.7 uses a thin
+legacy-response fallback. Server 1.5 standalone is limited to core graph,
+schema, data, and Gremlin operations; GraphSpace management is reported as
+unsupported. Version checks stay in the client adapter/resolver rather than
+being scattered through controllers or pages. See
+[`AGENTS.md`](AGENTS.md) for the support matrix and verification rules.
+
+### UI reference captures
+
+The following captures are collected from Chrome against the running Hubble
+application and document the supported states:
+
+![Authenticated connection and GraphSpace 
switch](docs/images/hubble-auth-connection.png)
+
+![Anonymous mode with account controls hidden](docs/images/hubble-non-auth.png)
+
+![GraphSpace permission presets](docs/images/hubble-graphspace-permissions.png)
+
+![PD connection discovered server 
address](docs/images/hubble-pd-connection.png)
+
+![Legacy compatibility capability 
notice](docs/images/hubble-legacy-capability.png)
+
 ## Local development feedback loop
 
 Run the frontend with third-party source-map noise disabled:
diff --git a/hugegraph-hubble/docs/images/hubble-auth-connection-root.png 
b/hugegraph-hubble/docs/images/hubble-auth-connection-root.png
new file mode 100644
index 000000000..966624185
Binary files /dev/null and 
b/hugegraph-hubble/docs/images/hubble-auth-connection-root.png differ
diff --git a/hugegraph-hubble/docs/images/hubble-auth-connection.png 
b/hugegraph-hubble/docs/images/hubble-auth-connection.png
new file mode 100644
index 000000000..f205e3621
Binary files /dev/null and 
b/hugegraph-hubble/docs/images/hubble-auth-connection.png differ
diff --git a/hugegraph-hubble/docs/images/hubble-graphspace-permissions.png 
b/hugegraph-hubble/docs/images/hubble-graphspace-permissions.png
new file mode 100644
index 000000000..06f4a0c3f
Binary files /dev/null and 
b/hugegraph-hubble/docs/images/hubble-graphspace-permissions.png differ
diff --git a/hugegraph-hubble/docs/images/hubble-legacy-capability.png 
b/hugegraph-hubble/docs/images/hubble-legacy-capability.png
new file mode 100644
index 000000000..982b789dc
Binary files /dev/null and 
b/hugegraph-hubble/docs/images/hubble-legacy-capability.png differ
diff --git a/hugegraph-hubble/docs/images/hubble-non-auth.png 
b/hugegraph-hubble/docs/images/hubble-non-auth.png
new file mode 100644
index 000000000..f205e3621
Binary files /dev/null and b/hugegraph-hubble/docs/images/hubble-non-auth.png 
differ
diff --git a/hugegraph-hubble/docs/images/hubble-pd-connection.png 
b/hugegraph-hubble/docs/images/hubble-pd-connection.png
new file mode 100644
index 000000000..8decb8bf6
Binary files /dev/null and 
b/hugegraph-hubble/docs/images/hubble-pd-connection.png differ
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java
index ec0fb77e8..27c96f601 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/WebMvcConfig.java
@@ -66,6 +66,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
         registry.addInterceptor(this.loginInterceptor())
                 .addPathPatterns("/api/**")
                 .excludePathPatterns("/api/**/auth/login")
+                .excludePathPatterns("/api/**/config")
                 .excludePathPatterns("/logout")
                 .excludePathPatterns("/api/**/auth/logout");
     }
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
index 807da39fc..db091fe14 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
@@ -26,6 +26,7 @@ import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.driver.factory.PDHugeClientFactory;
 import org.apache.hugegraph.options.HubbleOptions;
 import org.apache.hugegraph.service.auth.UserService;
+import org.apache.hugegraph.service.auth.AuthModeService;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.hugegraph.config.HugeConfig;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -59,6 +60,8 @@ public abstract class BaseController {
 
     @Autowired
     protected UserService userService;
+    @Autowired
+    protected AuthModeService authMode;
 
     public static final String ORDER_ASC = "asc";
     public static final String ORDER_DESC = "desc";
@@ -143,8 +146,13 @@ public abstract class BaseController {
             client.assignGraph(graphSpace, graph);
             return client;
         }
-        HugeClient client = this.hugeClientPoolService.createAuthClient(
-                graphSpace, graph, this.getToken());
+        HugeClient client = this.authMode.anonymous() ?
+                            this.hugeClientPoolService.createUnauthClient() :
+                            this.hugeClientPoolService.createAuthClient(
+                                graphSpace, graph, this.getToken());
+        if (graphSpace != null || graph != null) {
+            client.assignGraph(graphSpace, graph);
+        }
         request.setAttribute("hugeClient", client);
         return client;
     }
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
index 5b3795d8a..7815e82e8 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
@@ -41,6 +41,7 @@ public class ConfigController {
         Map<String, Object> result = new HashMap<>();
         boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED);
         result.put("pd_enabled", pdEnabled);
+        result.put("auth_enabled", config.get(HubbleOptions.AUTH_ENABLED));
         if (!pdEnabled) {
             result.put("server_url", config.get(HubbleOptions.SERVER_URL));
         }
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
index b59b24b74..3400d066b 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
@@ -75,6 +75,10 @@ public class LoginController extends BaseController {
 
     @PostMapping("/login")
     public Object login(@RequestBody Login login) {
+        if (this.authMode.anonymous()) {
+            throw new ExternalException(HttpStatus.FORBIDDEN.value(),
+                                        "Authentication is disabled");
+        }
         String address = this.getRequest().getRemoteAddr();
         boolean pdEnabled = this.config.get(HubbleOptions.PD_ENABLED);
         this.loginAttemptGuard.checkAllowed(login.name(), address);
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserEntity.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserEntity.java
index 9e3712819..f7953c5d3 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserEntity.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/entity/auth/UserEntity.java
@@ -30,6 +30,7 @@ import org.apache.hugegraph.common.Identifiable;
 
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 @Data
 @NoArgsConstructor
@@ -75,6 +76,12 @@ public class UserEntity implements Identifiable {
     @JsonProperty("resSpaces")
     protected List<String> resSpaces;
 
+    @JsonProperty("permission_preset")
+    private String permissionPreset;
+
+    @JsonProperty("graphspace_permissions")
+    private List<Map<String, String>> graphspacePermissions;
+
     @JsonProperty("spacenum")
     protected Integer spacenum;
 
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java
index b48d9cdbb..2bdc11f5a 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java
@@ -26,6 +26,7 @@ import javax.servlet.http.HttpSession;
 
 //import org.apache.hugegraph.license.LicenseVerifier; // TODO C Remove Licence
 import org.apache.hugegraph.service.HugeClientPoolService;
+import org.apache.hugegraph.service.auth.AuthModeService;
 //import org.apache.hugegraph.service.license.LicenseService;// TODO C Remove 
Licence
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
@@ -47,6 +48,8 @@ public class CustomInterceptor extends 
HandlerInterceptorAdapter {
     //private LicenseService licenseService;// TODO C Remove Licence
     @Autowired
     protected HugeClientPoolService hugeClientPoolService;
+    @Autowired
+    protected AuthModeService authMode;
 
     private static final Pattern CHECK_API_PATTERN =
                          Pattern.compile(".*/graph-connections/\\d+/.+");
@@ -119,23 +122,26 @@ public class CustomInterceptor extends 
HandlerInterceptorAdapter {
             if (this.isLogoutRequest(uri)) {
                 return;
             }
-            if (!this.hasAuthSession(request)) {
+            if (this.authMode.anonymous()) {
+                client = unauthClient();
+            } else if (!this.hasAuthSession(request)) {
                 return;
-            }
-            String token =
-                    (String) 
request.getSession().getAttribute(Constant.TOKEN_KEY);
-            String [] res = uri.split("/");
-            String graphSpace = null;
-            String graph = null;
-            for (int i = 0; i < res.length; i++) {
-                if ("graphspaces".equals(res[i]) && i < res.length - 1) {
-                    graphSpace = res[i + 1];
-                }
-                if ("graphs".equals(res[i]) && i < res.length - 1) {
-                    graph = res[i + 1];
+            } else {
+                String token =
+                        (String) 
request.getSession().getAttribute(Constant.TOKEN_KEY);
+                String [] res = uri.split("/");
+                String graphSpace = null;
+                String graph = null;
+                for (int i = 0; i < res.length; i++) {
+                    if ("graphspaces".equals(res[i]) && i < res.length - 1) {
+                        graphSpace = res[i + 1];
+                    }
+                    if ("graphs".equals(res[i]) && i < res.length - 1) {
+                        graph = res[i + 1];
+                    }
                 }
+                client = this.authClient(graphSpace, graph, token);
             }
-            client = this.authClient(graphSpace, graph, token);
         }
 
         request.setAttribute("hugeClient", client);
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java
index b0c1dd1ed..779c65c62 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java
@@ -20,6 +20,8 @@ package org.apache.hugegraph.handler;
 
 import org.apache.hugegraph.common.Constant;
 import org.apache.hugegraph.exception.UnauthorizedException;
+import org.apache.hugegraph.service.auth.AuthModeService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.StringUtils;
 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
 
@@ -28,6 +30,9 @@ import javax.servlet.http.HttpServletResponse;
 
 public class LoginInterceptor extends HandlerInterceptorAdapter {
 
+    @Autowired
+    private AuthModeService authMode;
+
     @Override
     public boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
@@ -35,6 +40,9 @@ public class LoginInterceptor extends 
HandlerInterceptorAdapter {
         if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
             return true;
         }
+        if (this.authMode != null && this.authMode.anonymous()) {
+            return true;
+        }
 
         if (!this.hasTextSessionAttribute(request, Constant.TOKEN_KEY) ||
             !this.hasTextSessionAttribute(request, Constant.USERNAME_KEY)) {
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java
index c04132c32..7cb589d1a 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java
@@ -279,6 +279,15 @@ public class HubbleOptions extends OptionHolder {
                     true
             );
 
+    public static final ConfigOption<Boolean> AUTH_ENABLED =
+            new ConfigOption<>(
+                    "auth.enabled",
+                    "Whether Hubble requires a user session. Set false when " +
+                    "the connected HugeGraph Server runs in anonymous mode.",
+                    null,
+                    true
+            );
+
     public static final ConfigOption<String> SERVER_URL =
             new ConfigOption<>(
                     "server.direct_url",
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
index dca1c4beb..b141b55de 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
@@ -87,6 +87,9 @@ public class AuthContextService {
     }
 
     public Map<String, Object> context(HugeClient client, String username) {
+        if (!this.config.get(HubbleOptions.AUTH_ENABLED)) {
+            return anonymousContext(this.config.get(HubbleOptions.PD_ENABLED));
+        }
         boolean pdEnabled = this.config.get(HubbleOptions.PD_ENABLED);
         String mode = pdEnabled ? "PD" : "NON_PD";
         String role;
@@ -125,6 +128,28 @@ public class AuthContextService {
         return Collections.unmodifiableMap(context);
     }
 
+    private static Map<String, Object> anonymousContext(boolean pdEnabled) {
+        Map<String, Object> context = new LinkedHashMap<>();
+        context.put("schema_version", SCHEMA_VERSION);
+        context.put("context_version", "anonymous-v1");
+        context.put("mode", "NON_AUTH");
+        context.put("username", null);
+        context.put("role", "ANONYMOUS");
+        Map<String, Set<String>> actions = new LinkedHashMap<>();
+        actions.put("graphspaces", pdEnabled ?
+                    Collections.singleton("read") : Collections.emptySet());
+        context.put("capabilities", pdEnabled ?
+                    set(GRAPH_RESOURCES_ACCESS, GRAPHSPACES_READ) :
+                    Collections.singleton(GRAPH_RESOURCES_ACCESS));
+        context.put("actions", actions);
+        Map<String, Object> scopes = new LinkedHashMap<>();
+        scopes.put("all_graphspaces", pdEnabled);
+        scopes.put("admin_graphspaces", Collections.emptyList());
+        scopes.put("graph_resources", "SERVER_ANONYMOUS");
+        context.put("scopes", scopes);
+        return Collections.unmodifiableMap(context);
+    }
+
     private Set<String> capabilities(boolean pdEnabled, String role) {
         Set<String> capabilities = new LinkedHashSet<>();
         capabilities.add(ACCOUNT_SELF_MANAGE);
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java
new file mode 100644
index 000000000..9500b709d
--- /dev/null
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.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.hugegraph.service.auth;
+
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.options.HubbleOptions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * Single boundary for Hubble authentication mode. Business controllers should
+ * not infer mode from sessions or PD settings.
+ */
+@Service
+public final class AuthModeService {
+
+    private final HugeConfig config;
+
+    @Autowired
+    public AuthModeService(HugeConfig config) {
+        this.config = config;
+    }
+
+    public boolean enabled() {
+        return this.config.get(HubbleOptions.AUTH_ENABLED);
+    }
+
+    public boolean anonymous() {
+        return !this.enabled();
+    }
+}
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
index cfdd501cf..ad8a669fa 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
@@ -20,7 +20,10 @@ package org.apache.hugegraph.service.auth;
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -32,6 +35,7 @@ import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.entity.auth.BelongEntity;
 import org.apache.hugegraph.entity.auth.RoleEntity;
 import org.apache.hugegraph.entity.auth.UserView;
+import org.apache.hugegraph.exception.ExternalException;
 import org.apache.hugegraph.structure.auth.User;
 import org.apache.hugegraph.util.E;
 import org.apache.hugegraph.util.PageUtil;
@@ -44,6 +48,8 @@ public class GraphSpaceUserService extends AuthService {
 
     @Autowired
     private BelongService belongService;
+    @Autowired
+    private RoleService roleService;
 
     public List<UserView> listUsers(HugeClient client, String graphSpace) {
         List<UserView> users = new ArrayList<>();
@@ -135,6 +141,90 @@ public class GraphSpaceUserService extends AuthService {
         return this.getUser(client, graphSpace, userView.getId());
     }
 
+    public void applyPermissionPresets(HugeClient client, String username,
+                                       List<Map<String, String>> permissions,
+                                       String preset) {
+        if (preset == null || "SUPER_ADMIN".equals(preset)) {
+            return;
+        }
+        User account = client.auth().getUserByName(username);
+        if (account == null) {
+            return;
+        }
+        Map<String, String> desired = new HashMap<>();
+        List<Map<String, String>> requested = permissions == null ?
+                                             new ArrayList<>() : permissions;
+        for (Map<String, String> permission : requested) {
+            String graphSpace = permission.get("graphspace");
+            String permissionPreset = permission.get("permission_preset");
+            if (graphSpace != null) {
+                desired.put(graphSpace,
+                           "GS_READ_ONLY".equals(permissionPreset) ?
+                           "observer" : 
"GS_READ_WRITE".equals(permissionPreset) ?
+                             "analyst" : null);
+            }
+        }
+        for (String graphSpace : client.graphSpace().listGraphSpace()) {
+            UserView current = this.getUser(client, graphSpace,
+                                             account.id().toString());
+            if (!desired.containsKey(graphSpace)) {
+                if (!current.getRoles().isEmpty() &&
+                    current.getRoles().stream().allMatch(
+                        GraphSpaceUserService::isPresetRole)) {
+                    this.unauthUser(client, graphSpace,
+                                    account.id().toString());
+                }
+                continue;
+            }
+            String presetRole = desired.get(graphSpace);
+            if (presetRole == null) {
+                List<RoleEntity> customRoles = current.getRoles().stream()
+                    .filter(existing -> !isPresetRole(existing))
+                    .collect(Collectors.toList());
+                if (customRoles.isEmpty() && !current.getRoles().isEmpty()) {
+                    this.unauthUser(client, graphSpace,
+                                    account.id().toString());
+                } else if (!customRoles.isEmpty()) {
+                    UserView view = new UserView(account.id().toString(),
+                                                 username, customRoles);
+                    this.createOrUpdate(client, graphSpace, view);
+                }
+                continue;
+            }
+            String role = this.resolvePresetRole(client, graphSpace,
+                                                 presetRole);
+            UserView view = new UserView(account.id().toString(), username,
+                                         new ArrayList<>());
+            view.addRole(new RoleEntity(role, role));
+            current.getRoles().stream()
+                   .filter(existing -> !isPresetRole(existing))
+                   .forEach(view::addRole);
+            this.createOrUpdate(client, graphSpace, view);
+        }
+    }
+
+    private String resolvePresetRole(HugeClient client, String graphSpace,
+                                     String roleName) {
+        return this.roleService.list(client, graphSpace, true).stream()
+                   .filter(role -> roleName.equalsIgnoreCase(role.name()) ||
+                                   roleName.equalsIgnoreCase(role.nickname()))
+                   .map(role -> role.id().toString())
+                   .findFirst()
+                   .orElseThrow(() -> new ExternalException(
+                           "auth.role.not-exist", roleName));
+    }
+
+    private static boolean isPresetRole(RoleEntity role) {
+        String name = role.getName() == null ? role.getId() :
+                      role.getName();
+        if (name == null) {
+            return false;
+        }
+        String normalized = name.toLowerCase(Locale.ROOT);
+        return "observer".equals(normalized) ||
+               "analyst".equals(normalized);
+    }
+
     public void unauthUser(HugeClient client, String graphSpace,
                            String userId) {
         User account = client.auth().getUser(userId);
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
index 035ba359b..6e696705f 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java
@@ -22,11 +22,14 @@ import java.io.File;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Locale;
 import java.util.stream.Collectors;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.apache.hugegraph.common.Constant;
 import org.apache.hugegraph.common.Response;
 import org.apache.hugegraph.structure.auth.Login;
@@ -39,6 +42,8 @@ import org.apache.hugegraph.config.HugeConfig;
 import org.apache.hugegraph.driver.AuthManager;
 import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.entity.auth.UserEntity;
+import org.apache.hugegraph.entity.auth.RoleEntity;
+import org.apache.hugegraph.entity.auth.UserView;
 import org.apache.hugegraph.exception.InternalException;
 import org.apache.hugegraph.structure.auth.User;
 import org.apache.hugegraph.util.HubbleUtil;
@@ -62,6 +67,8 @@ public class UserService extends AuthService {
 
     @Autowired
     private HugeConfig config;
+    @Autowired
+    private GraphSpaceUserService graphSpaceUserService;
 
     private boolean isPdEnabled() {
         return config.get(HubbleOptions.PD_ENABLED);
@@ -91,6 +98,7 @@ public class UserService extends AuthService {
                 user.setSpacenum(countMap.get(user.getName()));
                 user.setAdminSpaces(spaceMap.get(user.getName()));
             }
+            this.populatePermissionPresets(hugeClient, ues);
         }
 
         return ues;
@@ -125,6 +133,9 @@ public class UserService extends AuthService {
                 user.setAdminSpaces(spaceMap.get(user.getName()));
                 user.setSuperadmin(isSuperAdmin(hugeClient, user.getId()));
             }
+            IPage<UserEntity> page = PageUtil.page(results, pageNo, pageSize);
+            this.populatePermissionPresets(hugeClient, page.getRecords());
+            return page;
         } else {
             for (UserEntity user : results) {
                 user.setSuperadmin(isStandaloneAdmin(user.getName()));
@@ -158,6 +169,7 @@ public class UserService extends AuthService {
             userEntity.setAdminSpaces(adminSpaces);
             userEntity.setSpacenum(adminSpaces.size());
             userEntity.setResSpaces(resSpaces);
+            this.populatePermissionPresets(hugeClient, userEntity);
         } else {
             userEntity.setSuperadmin(isStandaloneAdmin(user.name()));
             userEntity.setAdminSpaces(new ArrayList<>());
@@ -192,6 +204,7 @@ public class UserService extends AuthService {
             userEntity.setAdminSpaces(adminSpaces);
             userEntity.setSpacenum(adminSpaces.size());
             userEntity.setResSpaces(resSpaces);
+            this.populatePermissionPresets(hugeClient, userEntity);
         } else {
             userEntity.setSuperadmin(isStandaloneAdmin(username));
             userEntity.setAdminSpaces(new ArrayList<>());
@@ -219,6 +232,11 @@ public class UserService extends AuthService {
                 client.auth().addSpaceAdmin(ue.getName(), graphspace);
             }
         }
+        if (isPdEnabled()) {
+            this.graphSpaceUserService.applyPermissionPresets(
+                    client, ue.getName(), ue.getGraphspacePermissions(),
+                    ue.getPermissionPreset());
+        }
 
         if (newUser != null && ue.isSuperadmin()) {
             // add superadmin
@@ -343,6 +361,71 @@ public class UserService extends AuthService {
         return u;
     }
 
+    private void populatePermissionPresets(HugeClient client,
+                                            UserEntity userEntity) {
+        List<Map<String, String>> permissions = new ArrayList<>();
+        List<String> graphSpaces = client.graphSpace().listGraphSpace();
+        for (String graphSpace : graphSpaces) {
+            if (userEntity.getAdminSpaces() != null &&
+                userEntity.getAdminSpaces().contains(graphSpace)) {
+                permissions.add(permission(graphSpace, "GS_ADMIN"));
+                continue;
+            }
+            UserView view = this.graphSpaceUserService.getUser(
+                    client, graphSpace, userEntity.getId());
+            view.getRoles().forEach(role -> {
+                String name = role.getName() == null ? "" :
+                             role.getName().toLowerCase(Locale.ROOT);
+                if ("observer".equals(name)) {
+                    permissions.add(permission(graphSpace, "GS_READ_ONLY"));
+                } else if ("analyst".equals(name)) {
+                    permissions.add(permission(graphSpace, "GS_READ_WRITE"));
+                }
+            });
+        }
+        userEntity.setGraphspacePermissions(permissions);
+    }
+
+    private void populatePermissionPresets(HugeClient client,
+                                            Collection<UserEntity> users) {
+        Map<String, List<Map<String, String>>> permissions = new HashMap<>();
+        for (String graphSpace : client.graphSpace().listGraphSpace()) {
+            for (UserView view : this.graphSpaceUserService.listUsers(
+                    client, graphSpace)) {
+                for (RoleEntity role : view.getRoles()) {
+                    String name = role.getName() == null ? "" :
+                                 role.getName().toLowerCase(Locale.ROOT);
+                    String preset = "observer".equals(name) ?
+                                    "GS_READ_ONLY" : "analyst".equals(name) ?
+                                    "GS_READ_WRITE" : null;
+                    if (preset != null) {
+                        permissions.computeIfAbsent(view.getId(),
+                            key -> new ArrayList<>()).add(
+                            permission(graphSpace, preset));
+                    }
+                }
+            }
+        }
+        for (UserEntity user : users) {
+            List<Map<String, String>> values = permissions.getOrDefault(
+                    user.getId(), new ArrayList<>());
+            if (user.getAdminSpaces() != null) {
+                for (String graphSpace : user.getAdminSpaces()) {
+                    values.add(permission(graphSpace, "GS_ADMIN"));
+                }
+            }
+            user.setGraphspacePermissions(values);
+        }
+    }
+
+    private static Map<String, String> permission(String graphSpace,
+                                                   String preset) {
+        Map<String, String> permission = new HashMap<>();
+        permission.put("graphspace", graphSpace);
+        permission.put("permission_preset", preset);
+        return permission;
+    }
+
     protected List<Object> getSpaceAndSpacenum(HugeClient hugeClient) {
         AuthManager auth = hugeClient.auth();
         List<Object> listMap = new ArrayList<>();
@@ -398,6 +481,12 @@ public class UserService extends AuthService {
         }
 
         hugeClient.auth().updateUser(user);
+        if (isPdEnabled()) {
+            this.graphSpaceUserService.applyPermissionPresets(
+                    hugeClient, userEntity.getName(),
+                    userEntity.getGraphspacePermissions(),
+                    userEntity.getPermissionPreset());
+        }
     }
 
     public void updatePersonal(HugeClient hugeClient, String username,
diff --git a/hugegraph-hubble/hubble-fe/src/App.js 
b/hugegraph-hubble/hubble-fe/src/App.js
index cf669d538..ec6ee6aa8 100644
--- a/hugegraph-hubble/hubble-fe/src/App.js
+++ b/hugegraph-hubble/hubble-fe/src/App.js
@@ -23,9 +23,49 @@ import './App.css';
 import './styles/workbench.scss';
 import Layout from './layout.ant';
 import {AuthContextProvider} from './auth/AuthContext';
+import * as api from './api';
+import {setConfig} from './utils/config';
+import {useEffect, useState} from 'react';
 
 function App() {
+    const [configReady, setConfigReady] = useState(false);
+    const [configError, setConfigError] = useState(false);
 
+    useEffect(() => {
+        let active = true;
+        api.config.getConfig()
+            .then(response => {
+                if (response?.status !== 200 || !response.data) {
+                    throw new Error('invalid_hubble_config');
+                }
+                if (active) {
+                    setConfig(response.data);
+                    setConfigReady(true);
+                }
+            })
+            .catch(() => {
+                if (active) {
+                    setConfigError(true);
+                }
+            });
+        return () => {
+            active = false;
+        };
+    }, []);
+
+    if (configError) {
+        return (
+            <div role='alert'>
+                Unable to load Hubble configuration.
+                <button type='button' onClick={() => window.location.reload()}>
+                    Retry
+                </button>
+            </div>
+        );
+    }
+    if (!configReady) {
+        return null;
+    }
     return (
         <div>
             <AuthContextProvider>
diff --git a/hugegraph-hubble/hubble-fe/src/auth/AuthContext.js 
b/hugegraph-hubble/hubble-fe/src/auth/AuthContext.js
index 1d9bc0a8d..6b553e7da 100644
--- a/hugegraph-hubble/hubble-fe/src/auth/AuthContext.js
+++ b/hugegraph-hubble/hubble-fe/src/auth/AuthContext.js
@@ -29,6 +29,7 @@ import {useLocation} from 'react-router-dom';
 import * as api from '../api/index';
 import {AUTH_REVALIDATE_EVENT} from '../utils/authEvents';
 import {getUser, USER_CHANGE_EVENT} from '../utils/user';
+import {isAuthEnabled} from '../utils/config';
 
 const REFRESH_INTERVAL_MS = 60_000;
 const MIN_REFRESH_INTERVAL_MS = 5_000;
@@ -41,6 +42,7 @@ const AuthContext = createContext({
 });
 
 const isSignedIn = () => Boolean(getUser()?.id);
+const shouldLoadContext = () => isSignedIn() || !isAuthEnabled();
 
 const unwrapContext = response => {
     if (response?.status !== 200 || !response.data
@@ -60,11 +62,11 @@ const AuthContextProvider = ({children}) => {
     const lastSuccessRef = useRef(0);
     const [identityEpoch, setIdentityEpoch] = useState(0);
     const [state, setState] = useState(() => (
-        isSignedIn() ? {...emptyState, loading: true} : emptyState
+        shouldLoadContext() ? {...emptyState, loading: true} : emptyState
     ));
 
     const load = useCallback(({force = false} = {}) => {
-        if (!isSignedIn()) {
+        if (!shouldLoadContext()) {
             setState(emptyState);
             return Promise.resolve(null);
         }
@@ -87,7 +89,7 @@ const AuthContextProvider = ({children}) => {
             .then(context => {
                 if (epoch === epochRef.current
                     && requestId === latestRequestRef.current
-                    && isSignedIn()) {
+                    && shouldLoadContext()) {
                     lastSuccessRef.current = Date.now();
                     setState({loading: false, context, error: null});
                 }
@@ -116,7 +118,7 @@ const AuthContextProvider = ({children}) => {
             inFlightRef.current = null;
             lastSuccessRef.current = 0;
             setIdentityEpoch(epochRef.current);
-            if (isSignedIn()) {
+            if (shouldLoadContext()) {
                 load({force: true}).catch(() => undefined);
             }
             else {
diff --git a/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js 
b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js
index 32cc7754e..0f095e0bd 100644
--- a/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js
+++ b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js
@@ -32,6 +32,7 @@ import {
     clearPersistedAlgorithmFormsForUser,
 } from '../../modules/algorithm/algorithmsForm/algorithmFormPersistence';
 import {useAuthContext} from '../../auth/AuthContext';
+import {isAuthEnabled} from '../../utils/config';
 
 const Topbar = () => {
     const userInfo = user.getUser();
@@ -50,7 +51,7 @@ const Topbar = () => {
     useEffect(() => {
         let cancelled = false;
 
-        if (!userInfo || !userInfo.id) {
+        if (isAuthEnabled() && (!userInfo || !userInfo.id)) {
             return undefined;
         }
 
@@ -69,7 +70,7 @@ const Topbar = () => {
         };
     }, [redirectToLogin, userInfo]);
 
-    if (!userInfo || !userInfo.id) {
+    if (isAuthEnabled() && (!userInfo || !userInfo.id)) {
         redirectToLogin();
     }
 
@@ -138,24 +139,26 @@ const Topbar = () => {
                     title={t('workbench.shortcuts.open_button')}
                     onClick={showShortcutHelp}
                 />
-                <Dropdown menu={userMenu} trigger={['click']}>
-                    <Button
-                        type='text'
-                        className={`${style.right} ${style.userMenuTrigger}`}
-                        aria-label={t('Topbar.user_menu', {name: userLabel})}
-                        aria-haspopup='menu'
-                        title={userLabel}
-                    >
-                        <Avatar
-                            size={'small'}
-                            icon={avatarLabel ? undefined : <UserOutlined />}
-                            aria-label={userLabel}
+                {isAuthEnabled() && userInfo?.id && (
+                    <Dropdown menu={userMenu} trigger={['click']}>
+                        <Button
+                            type='text'
+                            className={`${style.right} 
${style.userMenuTrigger}`}
+                            aria-label={t('Topbar.user_menu', {name: 
userLabel})}
+                            aria-haspopup='menu'
                             title={userLabel}
                         >
-                            {avatarLabel}
-                        </Avatar>
-                    </Button>
-                </Dropdown>
+                            <Avatar
+                                size={'small'}
+                                icon={avatarLabel ? undefined : <UserOutlined 
/>}
+                                aria-label={userLabel}
+                                title={userLabel}
+                            >
+                                {avatarLabel}
+                            </Avatar>
+                        </Button>
+                    </Dropdown>
+                )}
             </div>
         </Layout.Header>
     );
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
index 3c9f0fe08..e8b92359a 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
@@ -387,6 +387,12 @@
       "SPACEADMIN": "Space Administrator",
       "USER": "Regular User"
     },
+    "permission_preset": {
+      "SUPER_ADMIN": "Super Administrator",
+      "GS_READ_ONLY": "GraphSpace Read-only",
+      "GS_READ_WRITE": "GraphSpace Read-write",
+      "GS_ADMIN": "GraphSpace Administrator"
+    },
     "form": {
       "title_detail": "View Account",
       "title_edit": "Edit Account",
@@ -408,7 +414,11 @@
       "default_password_help": "The initial password for a new account. The 
user should change it after signing in",
       "default_password_placeholder": "Enter a 5–16 character initial 
password",
       "permission": "Admin Permission",
-      "permission_help": "Select the GraphSpaces this account may administer. 
HugeGraph roles control ordinary access"
+      "permission_help": "Select the GraphSpaces this account may administer. 
HugeGraph roles control ordinary access",
+      "permission_preset": "Access preset",
+      "permission_preset_help": "Choose one clear access preset; low-level 
roles and targets stay internal",
+      "graphspaces": "GraphSpaces",
+      "graphspaces_help": "Select the GraphSpaces covered by this preset"
     },
     "space_access": {
       "global_tab": "Global Accounts",
@@ -428,7 +438,8 @@
         "id": "Account ID",
         "name": "Account Name",
         "roles": "Roles",
-        "remove_confirm": "Remove this member from the GraphSpace?"
+        "remove_confirm": "Remove this member from the GraphSpace?",
+        "preset_unavailable": "The selected permission preset is not available 
on this GraphSpace."
       },
       "role": {
         "add": "Create Role",
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
index 93a5ae2a8..da3e7f7e0 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
@@ -387,6 +387,12 @@
       "SPACEADMIN": "空间管理员",
       "USER": "普通用户"
     },
+    "permission_preset": {
+      "SUPER_ADMIN": "超级管理员",
+      "GS_READ_ONLY": "GraphSpace 只读",
+      "GS_READ_WRITE": "GraphSpace 读写",
+      "GS_ADMIN": "GraphSpace 管理员"
+    },
     "form": {
       "title_detail": "查看账号",
       "title_edit": "编辑账号",
@@ -408,7 +414,11 @@
       "default_password_help": "创建账号时设置的初始密码;用户登录后应尽快修改",
       "default_password_placeholder": "请输入 5–16 位初始密码",
       "permission": "管理权限",
-      "permission_help": "选择该账号可以管理的图空间;普通访问权限由 HugeGraph 角色控制"
+      "permission_help": "选择该账号可以管理的图空间;普通访问权限由 HugeGraph 角色控制",
+      "permission_preset": "权限预设",
+      "permission_preset_help": "选择清晰的预设语义,底层角色与资源目标由系统维护",
+      "graphspaces": "GraphSpace",
+      "graphspaces_help": "选择该预设覆盖的 GraphSpace"
     },
     "space_access": {
       "global_tab": "全局账号",
@@ -428,7 +438,8 @@
         "id": "账号 ID",
         "name": "账号名",
         "roles": "角色",
-        "remove_confirm": "确定从该 GraphSpace 移除此成员吗?"
+        "remove_confirm": "确定从该 GraphSpace 移除此成员吗?",
+        "preset_unavailable": "当前 GraphSpace 不支持所选权限预设。"
       },
       "role": {
         "add": "创建角色",
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index 4fceadc32..8d5d43292 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -16,17 +16,25 @@
  * under the License.
  */
 
-import {Modal, Input, Form, Select, message, Spin, Switch} from 'antd';
+import {Modal, Input, Form, Select, message, Spin} from 'antd';
 import {useCallback, useEffect, useRef, useState} from 'react';
 import {useTranslation} from 'react-i18next';
 import * as api from '../../api';
 import * as rules from '../../utils/rules';
 import style from './index.module.scss';
 import FormHelpLabel from '../../components/FormHelpLabel';
-import {getAccountLevel} from './level';
+import {
+    getAccountPreset,
+    getPresetSpaces,
+    PERMISSION_PRESETS,
+    toPermissionPayload,
+} from './permissionPresets';
 
 const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
 const DEFAULT_ALLOWED_OPERATIONS = {create: true, edit: true, auth: true};
+const permissionPresetChanged = (prev, next) => (
+    prev.permission_preset !== next.permission_preset
+);
 
 const HelpLabel = ({t, labelKey}) => (
     <FormHelpLabel
@@ -60,7 +68,7 @@ const EditLayer = ({
     };
 
     const createUser = useCallback(values => {
-        return api.auth.addUser(values, PAGE_ERROR_CONFIG).then(res => {
+        return api.auth.addUser(toPermissionPayload(values), 
PAGE_ERROR_CONFIG).then(res => {
             if (res.status === 200) {
                 message.success(t('common.msg.create_success'));
                 onCancel();
@@ -71,7 +79,9 @@ const EditLayer = ({
         }).catch(() => message.error(t('common.msg.operation_failed')));
     }, [onCancel, refresh, t]);
     const updateUser = useCallback(values => {
-        return api.auth.updateUser(data.id, values, 
PAGE_ERROR_CONFIG).then(res => {
+        return api.auth.updateUser(
+            data.id, toPermissionPayload(values), PAGE_ERROR_CONFIG
+        ).then(res => {
             if (res.status === 200) {
                 message.success(t('common.msg.update_success'));
                 onCancel();
@@ -85,7 +95,13 @@ const EditLayer = ({
     }, [onCancel, refresh, data.id, t]);
 
     const updateUserAuth = useCallback(values => {
-        return api.auth.updateAdminspace(data.id, values.adminSpaces, 
PAGE_ERROR_CONFIG).then(res => {
+        const payload = toPermissionPayload({
+            ...values,
+            permission_preset: PERMISSION_PRESETS.GS_ADMIN,
+        });
+        return api.auth.updateAdminspace(
+            data.id, payload.adminSpaces, PAGE_ERROR_CONFIG
+        ).then(res => {
             if (res.status === 200) {
                 message.success(t('common.msg.set_success'));
                 onCancel();
@@ -176,7 +192,11 @@ const EditLayer = ({
 
                 if (res.status === 200) {
                     if (op !== 'detail') {
-                        form.setFieldsValue(res.data);
+                        form.setFieldsValue({
+                            ...res.data,
+                            permission_preset: getAccountPreset(res.data),
+                            graphspaces: getPresetSpaces(res.data),
+                        });
                     }
                     setDetail(res.data);
                     return;
@@ -233,17 +253,14 @@ const EditLayer = ({
                             <Form.Item label={t('account.form.name')} 
className={style.item}>
                                 {detail.user_nickname}
                             </Form.Item>
-                            <Form.Item label={t('account.form.is_superadmin')} 
className={style.item}>
-                                {detail.is_superadmin ? t('common.yes') : 
t('common.no')}
-                            </Form.Item>
-                            <Form.Item label={t('account.form.level')} 
className={style.item}>
-                                {t(`account.level.${getAccountLevel(detail)}`)}
+                            <Form.Item 
label={t('account.form.permission_preset')} className={style.item}>
+                                
{t(`account.permission_preset.${getAccountPreset(detail)}`)}
                             </Form.Item>
                             <Form.Item label={t('account.form.remark')} 
className={style.item}>
                                 {detail.user_description}
                             </Form.Item>
-                            <Form.Item label={t('account.form.permission')} 
className={style.item}>
-                                {detail.adminSpaces ? 
detail.adminSpaces.join(',') : ''}
+                            <Form.Item label={t('account.form.graphspaces')} 
className={style.item}>
+                                {getPresetSpaces(detail).join(', ')}
                             </Form.Item>
                             <Form.Item label={t('account.col.create_time')} 
className={style.item}>
                                 {detail.user_create}
@@ -291,11 +308,16 @@ const EditLayer = ({
                                         <Input 
placeholder={t('account.form.name_placeholder')} />
                                     </Form.Item>
                                     <Form.Item
-                                        label={<HelpLabel t={t} 
labelKey='account.form.is_superadmin' />}
-                                        name="is_superadmin"
-                                        valuePropName="checked"
+                                        label={<HelpLabel t={t} 
labelKey='account.form.permission_preset' />}
+                                        name="permission_preset"
+                                        rules={[rules.required()]}
                                     >
-                                        <Switch />
+                                        <Select
+                                            
options={Object.values(PERMISSION_PRESETS).map(value => ({
+                                                value,
+                                                label: 
t(`account.permission_preset.${value}`),
+                                            }))}
+                                        />
                                     </Form.Item>
                                     <Form.Item
                                         label={<HelpLabel t={t} 
labelKey='account.form.remark' />}
@@ -316,20 +338,42 @@ const EditLayer = ({
                                         />
                                     </Form.Item>
                                     <Form.Item
-                                        label={<HelpLabel t={t} 
labelKey='account.form.permission' />}
-                                        name="adminSpaces"
+                                        noStyle
+                                        shouldUpdate={permissionPresetChanged}
                                     >
-                                        <Select options={graphspaceList} 
mode="multiple" />
+                                        {({getFieldValue}) => 
(getFieldValue('permission_preset')
+                                            !== PERMISSION_PRESETS.GS_ADMIN ? 
null : (
+                                                <Form.Item
+                                                    label={<HelpLabel t={t} 
labelKey='account.form.graphspaces' />}
+                                                    name="graphspaces"
+                                                >
+                                                    <Select 
options={graphspaceList} mode="multiple" />
+                                                </Form.Item>
+                                            ))}
                                     </Form.Item>
                                 </>
                             )}
                             {op === 'auth' && (
-                                <Form.Item
-                                    label={<HelpLabel t={t} 
labelKey='account.form.permission' />}
-                                    name="adminSpaces"
-                                >
-                                    <Select options={graphspaceList} 
mode="multiple" />
-                                </Form.Item>
+                                <>
+                                    <Form.Item
+                                        label={<HelpLabel t={t} 
labelKey='account.form.permission_preset' />}
+                                        name="permission_preset"
+                                        rules={[rules.required()]}
+                                    >
+                                        <Select
+                                            
options={[PERMISSION_PRESETS.GS_ADMIN].map(value => ({
+                                                value,
+                                                label: 
t(`account.permission_preset.${value}`),
+                                            }))}
+                                        />
+                                    </Form.Item>
+                                    <Form.Item
+                                        label={<HelpLabel t={t} 
labelKey='account.form.graphspaces' />}
+                                        name="graphspaces"
+                                    >
+                                        <Select options={graphspaceList} 
mode="multiple" />
+                                    </Form.Item>
+                                </>
                             )}
                         </Form>
                     </Spin>
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
index 873e574b5..cd537c444 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
@@ -35,17 +35,45 @@ import {useTranslation} from 'react-i18next';
 import * as api from '../../api';
 import TableHeader from '../../components/TableHeader';
 import {useAuthContext} from '../../auth/AuthContext';
+import {PERMISSION_PRESETS} from './permissionPresets';
 
 const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
 const PAGE_PARAMS = {query: '', page_no: 1, page_size: 200};
-const PERMISSIONS = ['READ', 'WRITE', 'DELETE', 'EXECUTE'];
-const DEFAULT_RESOURCES = JSON.stringify([
-    {type: 'GREMLIN', label: '*', properties: null},
-], null, 2);
-
 const responseRecords = response => response?.data?.records ?? [];
-const responseList = response => (Array.isArray(response?.data) ? 
response.data : []);
-const accessRowKey = row => `${row.role_id}:${row.target_id}`;
+const rolePreset = role => {
+    const explicit = role?.permission_preset ?? role?.permissionPreset;
+    if (Object.values(PERMISSION_PRESETS).includes(explicit)) {
+        return explicit;
+    }
+    const names = [role?.role_name, role?.role_nickname, role?.name,
+        role?.nickname].filter(Boolean).map(name => name.toLowerCase());
+    if (names.includes('observer')) {
+        return PERMISSION_PRESETS.GS_READ_ONLY;
+    }
+    if (names.includes('analyst')) {
+        return PERMISSION_PRESETS.GS_READ_WRITE;
+    }
+    const permissions = role?.permissions ?? role?.actions;
+    if (Array.isArray(permissions)) {
+        if (permissions.includes('delete') || permissions.includes('admin')) {
+            return PERMISSION_PRESETS.GS_ADMIN;
+        }
+        if (permissions.includes('write') || permissions.includes('update')) {
+            return PERMISSION_PRESETS.GS_READ_WRITE;
+        }
+        if (permissions.includes('read')) {
+            return PERMISSION_PRESETS.GS_READ_ONLY;
+        }
+    }
+    return null;
+};
+
+const roleLabel = (role, t) => {
+    const preset = rolePreset(role);
+    return preset
+        ? t(`account.permission_preset.${preset}`)
+        : role?.role_name ?? role?.role_nickname ?? t('common.label.unknown');
+};
 
 const RowAction = ({row, onAction, children}) => {
     const handleClick = useCallback(() => onAction(row), [onAction, row]);
@@ -122,15 +150,8 @@ const SpaceAccess = () => {
     const contextVersion = context?.context_version;
     const scopes = context?.scopes ?? {};
     const memberActions = context?.actions?.members ?? [];
-    const roleActions = context?.actions?.roles ?? [];
-    const authorizationActions = context?.actions?.authorizations ?? [];
     const canAddMember = memberActions.includes('add');
     const canRemoveMember = memberActions.includes('remove');
-    const canCreateRole = roleActions.includes('create');
-    const canUpdateRole = roleActions.includes('update');
-    const canDeleteRole = roleActions.includes('delete');
-    const canGrant = authorizationActions.includes('grant');
-    const canRevoke = authorizationActions.includes('revoke');
     const [selectedSpace, setSelectedSpace] = useState('');
     const [allSpaces, setAllSpaces] = useState([]);
     const [spacesLoading, setSpacesLoading] = useState(false);
@@ -138,14 +159,8 @@ const SpaceAccess = () => {
     const [spacesRevision, setSpacesRevision] = useState(0);
     const spacesRequest = useRef(null);
     const [memberDialog, setMemberDialog] = useState(null);
-    const [roleDialog, setRoleDialog] = useState(null);
-    const [targetDialog, setTargetDialog] = useState(null);
-    const [accessDialog, setAccessDialog] = useState(null);
     const [submitting, setSubmitting] = useState(false);
     const [memberForm] = Form.useForm();
-    const [roleForm] = Form.useForm();
-    const [targetForm] = Form.useForm();
-    const [accessForm] = Form.useForm();
 
     const scopedSpaces = useMemo(
         () => scopes.admin_graphspaces ?? [],
@@ -200,31 +215,17 @@ const SpaceAccess = () => {
     const loadRoles = useCallback(space => api.auth.getSpaceRoles(
         space, PAGE_PARAMS, PAGE_ERROR_CONFIG
     ), []);
-    const loadTargets = useCallback(space => api.auth.getSpaceTargets(
-        space, PAGE_PARAMS, PAGE_ERROR_CONFIG
-    ), []);
-    const loadAccesses = useCallback(space => api.auth.getSpaceAccesses(
-        space, {}, PAGE_ERROR_CONFIG
-    ), []);
     const members = useScopedResource(
         graphSpace, contextVersion, loadMembers, responseRecords
     );
     const roles = useScopedResource(
         graphSpace, contextVersion, loadRoles, responseRecords
     );
-    const targets = useScopedResource(
-        graphSpace, contextVersion, loadTargets, responseRecords
-    );
-    const accesses = useScopedResource(
-        graphSpace, contextVersion, loadAccesses, responseList
-    );
 
     const refreshAll = useCallback(() => {
         members.retry();
         roles.retry();
-        targets.retry();
-        accesses.retry();
-    }, [accesses, members, roles, targets]);
+    }, [members, roles]);
 
     const runMutation = useCallback(async (operation, close) => {
         if (submitting) {
@@ -252,63 +253,26 @@ const SpaceAccess = () => {
     const openMember = useCallback(row => {
         memberForm.setFieldsValue({
             user_id: row?.user_id,
-            roles: row?.roles?.map(role => role.role_id) ?? [],
+            permission_preset: rolePreset(row?.roles?.[0]),
         });
         setMemberDialog(row ?? {});
     }, [memberForm]);
-    const openRole = useCallback(row => {
-        roleForm.setFieldsValue({
-            role_name: row?.role_name ?? row?.role_nickname,
-            role_description: row?.role_description,
-        });
-        setRoleDialog(row ?? {});
-    }, [roleForm]);
-    const openTarget = useCallback(row => {
-        targetForm.setFieldsValue({
-            target_name: row?.target_name,
-            target_graph: row?.target_graph,
-            target_description: row?.target_description,
-            target_resources: row?.target_resources
-                ? JSON.stringify(row.target_resources, null, 2)
-                : DEFAULT_RESOURCES,
-        });
-        setTargetDialog(row ?? {});
-    }, [targetForm]);
-    const openAccess = useCallback(row => {
-        accessForm.setFieldsValue({
-            role_id: row?.role_id,
-            target_id: row?.target_id,
-            permissions: row?.permissions ?? [],
-        });
-        setAccessDialog(row ?? {});
-    }, [accessForm]);
-
     const closeMember = useCallback(() => {
         setMemberDialog(null);
         memberForm.resetFields();
     }, [memberForm]);
-    const closeRole = useCallback(() => {
-        setRoleDialog(null);
-        roleForm.resetFields();
-    }, [roleForm]);
-    const closeTarget = useCallback(() => {
-        setTargetDialog(null);
-        targetForm.resetFields();
-    }, [targetForm]);
-    const closeAccess = useCallback(() => {
-        setAccessDialog(null);
-        accessForm.resetFields();
-    }, [accessForm]);
-
     const submitMember = useCallback(values => {
-        const roleLookup = new Map(roles.data.map(role => [role.id, role]));
+        const role = roles.data.find(item => rolePreset(item) === 
values.permission_preset);
+        if (!role) {
+            message.error(t('account.space_access.member.preset_unavailable'));
+            return;
+        }
         const payload = {
             user_id: values.user_id,
-            roles: values.roles.map(id => ({
-                role_id: id,
-                role_name: roleLookup.get(id)?.role_name
-                           ?? roleLookup.get(id)?.role_nickname ?? id,
-            })),
+            roles: role ? [{
+                role_id: role.id,
+                role_name: role.role_name ?? role.role_nickname ?? 
values.permission_preset,
+            }] : [],
         };
         const operation = memberDialog?.user_id
             ? () => api.auth.updateSpaceMember(
@@ -318,54 +282,7 @@ const SpaceAccess = () => {
                 graphSpace, payload, PAGE_ERROR_CONFIG
             );
         runMutation(operation, closeMember);
-    }, [closeMember, graphSpace, memberDialog, roles.data, runMutation]);
-
-    const submitRole = useCallback(values => {
-        const operation = roleDialog?.id
-            ? () => api.auth.updateSpaceRole(
-                graphSpace, roleDialog.id, values, PAGE_ERROR_CONFIG
-            )
-            : () => api.auth.addSpaceRole(
-                graphSpace, values, PAGE_ERROR_CONFIG
-            );
-        runMutation(operation, closeRole);
-    }, [closeRole, graphSpace, roleDialog, runMutation]);
-
-    const submitTarget = useCallback(values => {
-        let resources;
-        try {
-            resources = JSON.parse(values.target_resources);
-            if (!Array.isArray(resources)) {
-                throw new Error('resources must be an array');
-            }
-        }
-        catch (error) {
-            targetForm.setFields([{
-                name: 'target_resources',
-                errors: [t('account.space_access.target.resources_invalid')],
-            }]);
-            return;
-        }
-        const payload = {
-            target_name: values.target_name,
-            target_graph: values.target_graph,
-            target_description: values.target_description,
-            target_resources: resources,
-        };
-        const operation = targetDialog?.id
-            ? () => api.auth.updateSpaceTarget(
-                graphSpace, targetDialog.id, payload, PAGE_ERROR_CONFIG
-            )
-            : () => api.auth.addSpaceTarget(
-                graphSpace, payload, PAGE_ERROR_CONFIG
-            );
-        runMutation(operation, closeTarget);
-    }, [closeTarget, graphSpace, runMutation, t, targetDialog, targetForm]);
-
-    const submitAccess = useCallback(values => {
-        runMutation(() => api.auth.saveSpaceAccess(graphSpace, values,
-            PAGE_ERROR_CONFIG), closeAccess);
-    }, [closeAccess, graphSpace, runMutation]);
+    }, [closeMember, graphSpace, memberDialog, roles.data, runMutation, t]);
 
     const confirmDelete = useCallback((title, operation) => {
         Modal.confirm({
@@ -381,34 +298,11 @@ const SpaceAccess = () => {
             graphSpace, row.user_id, PAGE_ERROR_CONFIG
         )
     ), [confirmDelete, graphSpace, t]);
-    const editRole = useCallback(row => openRole(row), [openRole]);
-    const deleteRole = useCallback(row => confirmDelete(
-        t('account.space_access.role.delete_confirm'),
-        () => api.auth.deleteSpaceRole(graphSpace, row.id, PAGE_ERROR_CONFIG)
-    ), [confirmDelete, graphSpace, t]);
-    const editTarget = useCallback(row => openTarget(row), [openTarget]);
-    const deleteTarget = useCallback(row => confirmDelete(
-        t('account.space_access.target.delete_confirm'),
-        () => api.auth.deleteSpaceTarget(graphSpace, row.id, PAGE_ERROR_CONFIG)
-    ), [confirmDelete, graphSpace, t]);
-    const editAccess = useCallback(row => openAccess(row), [openAccess]);
-    const deleteAccess = useCallback(row => confirmDelete(
-        t('account.space_access.authorization.delete_confirm'),
-        () => api.auth.deleteSpaceAccess(
-            graphSpace, row.role_id, row.target_id, PAGE_ERROR_CONFIG
-        )
-    ), [confirmDelete, graphSpace, t]);
     const addMember = useCallback(() => openMember(), [openMember]);
-    const addRole = useCallback(() => openRole(), [openRole]);
-    const addTarget = useCallback(() => openTarget(), [openTarget]);
-    const addAccess = useCallback(() => openAccess(), [openAccess]);
     const retrySpaces = useCallback(
         () => setSpacesRevision(value => value + 1), []
     );
     const submitMemberForm = useCallback(() => memberForm.submit(), 
[memberForm]);
-    const submitRoleForm = useCallback(() => roleForm.submit(), [roleForm]);
-    const submitTargetForm = useCallback(() => targetForm.submit(), 
[targetForm]);
-    const submitAccessForm = useCallback(() => accessForm.submit(), 
[accessForm]);
 
     const memberColumns = [
         {title: t('account.space_access.member.id'), dataIndex: 'user_id'},
@@ -417,7 +311,7 @@ const SpaceAccess = () => {
             title: t('account.space_access.member.roles'),
             dataIndex: 'roles',
             render: value => value?.map(role => (
-                <Tag key={role.role_id}>{role.role_name}</Tag>
+                <Tag key={role.role_id}>{roleLabel(role, t)}</Tag>
             )),
         },
         ...((canAddMember || canRemoveMember) ? [{
@@ -439,86 +333,6 @@ const SpaceAccess = () => {
         }] : []),
     ];
 
-    const roleColumns = [
-        {title: t('account.space_access.role.name'), dataIndex: 'role_name'},
-        {
-            title: t('account.space_access.role.description'),
-            dataIndex: 'role_description',
-        },
-        ...((canUpdateRole || canDeleteRole) ? [{
-            title: t('common.operation'),
-            render: row => (
-                <Space>
-                    {canUpdateRole && (
-                        <RowAction row={row} onAction={editRole}>
-                            {t('common.action.edit')}
-                        </RowAction>
-                    )}
-                    {canDeleteRole && (
-                        <RowAction row={row} onAction={deleteRole}>
-                            {t('common.action.delete')}
-                        </RowAction>
-                    )}
-                </Space>
-            ),
-        }] : []),
-    ];
-
-    const targetColumns = [
-        {title: t('account.space_access.target.name'), dataIndex: 
'target_name'},
-        {title: t('account.space_access.target.graph'), dataIndex: 
'target_graph'},
-        {
-            title: t('account.space_access.target.description'),
-            dataIndex: 'target_description',
-        },
-        ...((canGrant || canRevoke) ? [{
-            title: t('common.operation'),
-            render: row => (
-                <Space>
-                    {canGrant && (
-                        <RowAction row={row} onAction={editTarget}>
-                            {t('common.action.edit')}
-                        </RowAction>
-                    )}
-                    {canRevoke && (
-                        <RowAction row={row} onAction={deleteTarget}>
-                            {t('common.action.delete')}
-                        </RowAction>
-                    )}
-                </Space>
-            ),
-        }] : []),
-    ];
-
-    const accessColumns = [
-        {title: t('account.space_access.role.name'), dataIndex: 'role_name'},
-        {title: t('account.space_access.target.name'), dataIndex: 
'target_name'},
-        {
-            title: t('account.space_access.authorization.permissions'),
-            dataIndex: 'permissions',
-            render: value => value?.map(permission => (
-                <Tag key={permission}>{permission}</Tag>
-            )),
-        },
-        ...((canGrant || canRevoke) ? [{
-            title: t('common.operation'),
-            render: row => (
-                <Space>
-                    {canGrant && (
-                        <RowAction row={row} onAction={editAccess}>
-                            {t('common.action.edit')}
-                        </RowAction>
-                    )}
-                    {canRevoke && (
-                        <RowAction row={row} onAction={deleteAccess}>
-                            {t('common.action.delete')}
-                        </RowAction>
-                    )}
-                </Space>
-            ),
-        }] : []),
-    ];
-
     const table = (resource, columns, rowKey, addLabel, onAdd, canAdd) => (
         <>
             <ErrorAlert error={resource.error} retry={resource.retry} t={t} />
@@ -583,34 +397,6 @@ const SpaceAccess = () => {
                             addMember, canAddMember
                         ),
                     },
-                    {
-                        key: 'roles',
-                        label: t('account.space_access.tabs.roles'),
-                        children: table(
-                            roles, roleColumns, 'id',
-                            t('account.space_access.role.add'),
-                            addRole, canCreateRole
-                        ),
-                    },
-                    {
-                        key: 'targets',
-                        label: t('account.space_access.tabs.targets'),
-                        children: table(
-                            targets, targetColumns, 'id',
-                            t('account.space_access.target.add'),
-                            addTarget, canGrant
-                        ),
-                    },
-                    {
-                        key: 'authorizations',
-                        label: t('account.space_access.tabs.authorizations'),
-                        children: table(
-                            accesses, accessColumns,
-                            accessRowKey,
-                            t('account.space_access.authorization.add'),
-                            addAccess, canGrant
-                        ),
-                    },
                 ]}
             />
 
@@ -631,135 +417,22 @@ const SpaceAccess = () => {
                         <Input disabled={Boolean(memberDialog?.user_id)} />
                     </Form.Item>
                     <Form.Item
-                        name="roles"
+                        name="permission_preset"
                         label={t('account.space_access.member.roles')}
-                        rules={[{required: true, type: 'array', min: 1}]}
-                    >
-                        <Select
-                            mode="multiple"
-                            options={roles.data.map(role => ({
-                                value: role.id,
-                                label: role.role_name ?? role.role_nickname,
-                            }))}
-                        />
-                    </Form.Item>
-                </Form>
-            </Modal>
-
-            <Modal
-                open={roleDialog !== null}
-                title={t('account.space_access.role.dialog')}
-                onCancel={closeRole}
-                onOk={submitRoleForm}
-                confirmLoading={submitting}
-                destroyOnClose
-            >
-                <Form form={roleForm} layout="vertical" onFinish={submitRole}>
-                    <Form.Item
-                        name="role_name"
-                        label={t('account.space_access.role.name')}
-                        rules={[{required: true}]}
-                    >
-                        <Input />
-                    </Form.Item>
-                    <Form.Item
-                        name="role_description"
-                        label={t('account.space_access.role.description')}
-                    >
-                        <Input />
-                    </Form.Item>
-                </Form>
-            </Modal>
-
-            <Modal
-                open={targetDialog !== null}
-                title={t('account.space_access.target.dialog')}
-                onCancel={closeTarget}
-                onOk={submitTargetForm}
-                confirmLoading={submitting}
-                destroyOnClose
-            >
-                <Form form={targetForm} layout="vertical" 
onFinish={submitTarget}>
-                    <Form.Item
-                        name="target_name"
-                        label={t('account.space_access.target.name')}
-                        rules={[{required: true}]}
-                    >
-                        <Input disabled={Boolean(targetDialog?.id)} />
-                    </Form.Item>
-                    <Form.Item
-                        name="target_graph"
-                        label={t('account.space_access.target.graph')}
                         rules={[{required: true}]}
-                    >
-                        <Input disabled={Boolean(targetDialog?.id)} />
-                    </Form.Item>
-                    <Form.Item
-                        name="target_description"
-                        label={t('account.space_access.target.description')}
-                    >
-                        <Input />
-                    </Form.Item>
-                    <Form.Item
-                        name="target_resources"
-                        label={t('account.space_access.target.resources')}
-                        rules={[{required: true}]}
-                    >
-                        <Input.TextArea autoSize={{minRows: 5, maxRows: 12}} />
-                    </Form.Item>
-                </Form>
-            </Modal>
-
-            <Modal
-                open={accessDialog !== null}
-                title={t('account.space_access.authorization.dialog')}
-                onCancel={closeAccess}
-                onOk={submitAccessForm}
-                confirmLoading={submitting}
-                destroyOnClose
-            >
-                <Form form={accessForm} layout="vertical" 
onFinish={submitAccess}>
-                    <Form.Item
-                        name="role_id"
-                        label={t('account.space_access.role.name')}
-                        rules={[{required: true}]}
-                    >
-                        <Select
-                            disabled={Boolean(accessDialog?.role_id)}
-                            options={roles.data.map(role => ({
-                                value: role.id,
-                                label: role.role_name ?? role.role_nickname,
-                            }))}
-                        />
-                    </Form.Item>
-                    <Form.Item
-                        name="target_id"
-                        label={t('account.space_access.target.name')}
-                        rules={[{required: true}]}
-                    >
-                        <Select
-                            disabled={Boolean(accessDialog?.target_id)}
-                            options={targets.data.map(target => ({
-                                value: target.id,
-                                label: target.target_name,
-                            }))}
-                        />
-                    </Form.Item>
-                    <Form.Item
-                        name="permissions"
-                        
label={t('account.space_access.authorization.permissions')}
-                        rules={[{required: true, type: 'array', min: 1}]}
                     >
                         <Select
-                            mode="multiple"
-                            options={PERMISSIONS.map(permission => ({
-                                value: permission,
-                                label: permission,
-                            }))}
+                            options={Object.values(PERMISSION_PRESETS)
+                                .filter(value => value !== 
PERMISSION_PRESETS.SUPER_ADMIN)
+                                .map(value => ({
+                                    value,
+                                    label: 
t(`account.permission_preset.${value}`),
+                                }))}
                         />
                     </Form.Item>
                 </Form>
             </Modal>
+
         </>
     );
 };
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
index 5fbd54c8f..a74866cc8 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import {act, render, screen, waitFor} from '@testing-library/react';
+import {act, fireEvent, render, screen, waitFor} from '@testing-library/react';
 import SpaceAccess from './SpaceAccess';
 import * as api from '../../api';
 
@@ -109,12 +109,8 @@ test('uses only path-scoped APIs for a space 
administrator', async () => {
     expect(api.auth.getSpaceRoles).toHaveBeenCalledWith(
         'SPACE_A', expect.any(Object), expect.any(Object)
     );
-    expect(api.auth.getSpaceTargets).toHaveBeenCalledWith(
-        'SPACE_A', expect.any(Object), expect.any(Object)
-    );
-    expect(api.auth.getSpaceAccesses).toHaveBeenCalledWith(
-        'SPACE_A', expect.any(Object), expect.any(Object)
-    );
+    expect(api.auth.getSpaceTargets).not.toHaveBeenCalled();
+    expect(api.auth.getSpaceAccesses).not.toHaveBeenCalled();
     expect(api.auth.getAllUserList).not.toHaveBeenCalled();
     expect(api.manage.getGraphSpaceList).not.toHaveBeenCalled();
 });
@@ -172,3 +168,54 @@ test('does not infer mutations when the server grants 
read-only actions', async
         name: 'common.action.delete',
     })).not.toBeInTheDocument();
 });
+
+test('maps a selected preset to the authoritative role id when adding a 
member', async () => {
+    api.auth.getSpaceRoles.mockResolvedValueOnce(page([{
+        id: 'writer-id',
+        permission_preset: 'GS_READ_WRITE',
+    }]));
+    api.auth.addSpaceMember.mockResolvedValue({status: 200});
+    render(<SpaceAccess />);
+
+    await screen.findAllByText('alice');
+    fireEvent.click(screen.getByRole('button', {
+        name: 'account.space_access.member.add',
+    }));
+    const textboxes = screen.getAllByRole('textbox');
+    fireEvent.change(textboxes[textboxes.length - 1], {
+        target: {value: 'bob'},
+    });
+    const comboboxes = screen.getAllByRole('combobox');
+    fireEvent.mouseDown(comboboxes[comboboxes.length - 1]);
+    
fireEvent.click(screen.getByText('account.permission_preset.GS_READ_WRITE'));
+    fireEvent.click(screen.getByRole('button', {name: 'OK'}));
+
+    await waitFor(() => expect(api.auth.addSpaceMember).toHaveBeenCalledWith(
+        'SPACE_A',
+        {
+            user_id: 'bob',
+            roles: [{role_id: 'writer-id', role_name: 'GS_READ_WRITE'}],
+        },
+        expect.any(Object)
+    ));
+});
+
+test('does not submit when the selected preset has no server role', async () 
=> {
+    api.auth.addSpaceMember.mockResolvedValue({status: 200});
+    render(<SpaceAccess />);
+
+    await screen.findAllByText('alice');
+    fireEvent.click(screen.getByRole('button', {
+        name: 'account.space_access.member.add',
+    }));
+    const textboxes = screen.getAllByRole('textbox');
+    fireEvent.change(textboxes[textboxes.length - 1], {
+        target: {value: 'bob'},
+    });
+    const comboboxes = screen.getAllByRole('combobox');
+    fireEvent.mouseDown(comboboxes[comboboxes.length - 1]);
+    
fireEvent.click(screen.getByText('account.permission_preset.GS_READ_WRITE'));
+    fireEvent.click(screen.getByRole('button', {name: 'OK'}));
+
+    await waitFor(() => 
expect(api.auth.addSpaceMember).not.toHaveBeenCalled());
+});
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
index 04c3d7126..c08a3407c 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
@@ -146,7 +146,7 @@ test('loads graphspaces into the visible create account 
form', async () => {
         status: 200,
         data: {records: [{name: 'analytics'}]},
     }));
-    fireEvent.mouseDown(screen.getByRole('combobox'));
+    fireEvent.mouseDown(screen.getAllByRole('combobox')[1]);
 
     expect(await screen.findByRole('option', {name: 
'analytics'})).toBeInTheDocument();
 });
@@ -172,5 +172,7 @@ test('shows the derived space administrator level in 
account details', async ()
 
     render(<EditLayer {...props} data={{id: 'space-admin'}} op='detail' />);
 
-    expect(await 
screen.findByText('account.level.SPACEADMIN')).toBeInTheDocument();
+    expect(await screen.findByText(
+        'account.permission_preset.GS_ADMIN'
+    )).toBeInTheDocument();
 });
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
index 7b3566887..ad74bcc28 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
@@ -111,9 +111,12 @@ test('labels administrators, space administrators, and 
regular users in the list
 
     render(<Account />);
 
-    expect(await screen.findByText('account.level.ADMIN')).toBeInTheDocument();
-    expect(screen.getByText('account.level.SPACEADMIN')).toBeInTheDocument();
-    expect(screen.getByText('account.level.USER')).toBeInTheDocument();
+    expect(await screen.findByText('account.permission_preset.SUPER_ADMIN'))
+        .toBeInTheDocument();
+    expect(screen.getByText('account.permission_preset.GS_ADMIN'))
+        .toBeInTheDocument();
+    expect(screen.getByText('account.permission_preset.GS_READ_ONLY'))
+        .toBeInTheDocument();
 });
 
 test('space administrators use scoped management without loading global 
accounts', async () => {
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
index e1f5f0efa..7c0bd4aae 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
@@ -34,7 +34,7 @@ import TableHeader from '../../components/TableHeader';
 import EditLayer from './EditLayer';
 import * as api from '../../api';
 import {useAuthContext} from '../../auth/AuthContext';
-import {getAccountLevel} from './level';
+import {getAccountPreset, PERMISSION_PRESETS} from './permissionPresets';
 import SpaceAccess from './SpaceAccess';
 
 const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
@@ -136,10 +136,10 @@ const GlobalAccounts = () => {
             title: t('account.col.level'),
             width: 140,
             render: row => {
-                const level = getAccountLevel(row);
-                const color = level === 'ADMIN' ? 'red'
-                    : level === 'SPACEADMIN' ? 'blue' : 'default';
-                return <Tag color={color}>{t(`account.level.${level}`)}</Tag>;
+                const preset = getAccountPreset(row);
+                const color = preset === PERMISSION_PRESETS.SUPER_ADMIN ? 'red'
+                    : preset === PERMISSION_PRESETS.GS_ADMIN ? 'blue' : 
'default';
+                return <Tag 
color={color}>{t(`account.permission_preset.${preset}`)}</Tag>;
             },
         },
         {
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.js
new file mode 100644
index 000000000..99fb9a64e
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+const PERMISSION_PRESETS = Object.freeze({
+    SUPER_ADMIN: 'SUPER_ADMIN',
+    GS_READ_ONLY: 'GS_READ_ONLY',
+    GS_READ_WRITE: 'GS_READ_WRITE',
+    GS_ADMIN: 'GS_ADMIN',
+});
+
+const presetKeys = Object.values(PERMISSION_PRESETS);
+
+const getAccountPreset = account => {
+    const explicit = account?.permission_preset
+        ?? account?.permissionPreset
+        ?? account?.access_level;
+    if (presetKeys.includes(explicit)) {
+        return explicit;
+    }
+    if (account?.is_superadmin) {
+        return PERMISSION_PRESETS.SUPER_ADMIN;
+    }
+    const scopedPresets = (account?.graphspace_permissions ?? [])
+        .map(permission => permission?.permission_preset)
+        .filter(preset => presetKeys.includes(preset));
+    if (scopedPresets.includes(PERMISSION_PRESETS.GS_ADMIN)) {
+        return PERMISSION_PRESETS.GS_ADMIN;
+    }
+    if (scopedPresets.includes(PERMISSION_PRESETS.GS_READ_WRITE)) {
+        return PERMISSION_PRESETS.GS_READ_WRITE;
+    }
+    if (Array.isArray(account?.adminSpaces) && account.adminSpaces.length > 0) 
{
+        return PERMISSION_PRESETS.GS_ADMIN;
+    }
+    return PERMISSION_PRESETS.GS_READ_ONLY;
+};
+
+const getPresetSpaces = account => {
+    const spaces = account?.graphspace_permissions ?? account?.adminSpaces;
+    return Array.isArray(spaces)
+        ? spaces.map(space => (typeof space === 'string'
+            ? space : space.name ?? space.graphspace)).filter(Boolean)
+        : [];
+};
+
+// This is the only compatibility mapping used by account forms.
+const toPermissionPayload = values => {
+    const preset = values.permission_preset ?? PERMISSION_PRESETS.GS_READ_ONLY;
+    const spaces = values.graphspaces ?? [];
+    const payload = {...values};
+    delete payload.permission_preset;
+    delete payload.graphspaces;
+    return {
+        ...payload,
+        permission_preset: preset,
+        graphspace_permissions: spaces.map(graphspace => ({
+            graphspace,
+            permission_preset: preset,
+        })),
+        is_superadmin: preset === PERMISSION_PRESETS.SUPER_ADMIN,
+        adminSpaces: preset === PERMISSION_PRESETS.GS_ADMIN ? spaces : [],
+    };
+};
+
+export {PERMISSION_PRESETS, getAccountPreset, getPresetSpaces, 
toPermissionPayload};
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.test.js
new file mode 100644
index 000000000..cf287cae4
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.test.js
@@ -0,0 +1,70 @@
+/*
+ * 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 {
+    getAccountPreset,
+    getPresetSpaces,
+    PERMISSION_PRESETS,
+    toPermissionPayload,
+} from './permissionPresets';
+
+test.each([
+    [{is_superadmin: true}, PERMISSION_PRESETS.SUPER_ADMIN],
+    [{permission_preset: 'GS_READ_WRITE'}, PERMISSION_PRESETS.GS_READ_WRITE],
+    [{graphspace_permissions: [{
+        graphspace: 'team',
+        permission_preset: 'GS_READ_WRITE',
+    }]}, PERMISSION_PRESETS.GS_READ_WRITE],
+    [{adminSpaces: ['team']}, PERMISSION_PRESETS.GS_ADMIN],
+    [{adminSpaces: []}, PERMISSION_PRESETS.GS_READ_ONLY],
+])('normalizes account %j to %s', (account, expected) => {
+    expect(getAccountPreset(account)).toBe(expected);
+});
+
+test('normalizes GraphSpace objects and keeps legacy payload in one adapter', 
() => {
+    expect(getPresetSpaces({adminSpaces: [{name: 'team'}]})).toEqual(['team']);
+    expect(getPresetSpaces({
+        graphspace_permissions: [{graphspace: 'team'}],
+    })).toEqual(['team']);
+    expect(toPermissionPayload({
+        permission_preset: PERMISSION_PRESETS.GS_ADMIN,
+        graphspaces: ['team'],
+    })).toMatchObject({
+        adminSpaces: ['team'],
+        is_superadmin: false,
+    });
+    expect(toPermissionPayload({
+        permission_preset: PERMISSION_PRESETS.GS_READ_WRITE,
+        graphspaces: ['team'],
+    })).toMatchObject({
+        permission_preset: PERMISSION_PRESETS.GS_READ_WRITE,
+        graphspace_permissions: [{
+            graphspace: 'team',
+            permission_preset: PERMISSION_PRESETS.GS_READ_WRITE,
+        }],
+    });
+});
+
+test('clears admin spaces for the super administrator preset', () => {
+    expect(toPermissionPayload({
+        permission_preset: PERMISSION_PRESETS.SUPER_ADMIN,
+        graphspaces: ['team'],
+    })).toMatchObject({
+        adminSpaces: [],
+        is_superadmin: true,
+    });
+});
diff --git a/hugegraph-hubble/hubble-fe/src/routes/index.js 
b/hugegraph-hubble/hubble-fe/src/routes/index.js
index 34789a2ce..8d7a27599 100644
--- a/hugegraph-hubble/hubble-fe/src/routes/index.js
+++ b/hugegraph-hubble/hubble-fe/src/routes/index.js
@@ -42,7 +42,7 @@ import OperationsRoute, {
 // 图分析的路由
 import GraphAnalysis from '../pages/GraphAnalysis';
 import AsyncTaskResultPage from '../pages/AsyncTaskResult';
-import {isPdEnabled} from '../utils/config';
+import {isAuthEnabled, isPdEnabled} from '../utils/config';
 import {
     DEFAULT_GRAPHSPACE,
     shouldUseNonPdDefaultGraphspace,
@@ -64,8 +64,12 @@ const isLoggedIn = () => {
 
 const ProtectedRoute = ({children}) => {
     const location = useLocation();
+    // Keep older test/integration shims that only expose PD configuration
+    // conservative until they learn the auth capability.
+    const authEnabled = typeof isAuthEnabled === 'function'
+        ? isAuthEnabled() : true;
 
-    if (isLoggedIn()) {
+    if (!authEnabled || isLoggedIn()) {
         return children;
     }
 
diff --git a/hugegraph-hubble/hubble-fe/src/utils/config.js 
b/hugegraph-hubble/hubble-fe/src/utils/config.js
index 55964130e..e17acbf93 100644
--- a/hugegraph-hubble/hubble-fe/src/utils/config.js
+++ b/hugegraph-hubble/hubble-fe/src/utils/config.js
@@ -31,4 +31,8 @@ const isPdEnabled = () => {
     return getConfig().pd_enabled;
 };
 
-export {setConfig, getConfig, isPdEnabled};
+const isAuthEnabled = () => {
+    return getConfig().auth_enabled !== false;
+};
+
+export {setConfig, getConfig, isPdEnabled, isAuthEnabled};

Reply via email to