You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/08/30 10:58:21 UTC

(hugegraph-toolchain) 06/24: fix(hubble): harden account template boundaries

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 4ef08b628015614cd7705ca7023ae64a7ab1e2aa
Author: dark <ji...@apache.org>
AuthorDate: Tue Aug 18 18:59:43 2026 +0800

    fix(hubble): harden account template boundaries
    
    - enforce template ownership and pagination
    - reset stale account and GraphSpace context
    - separate account identities
    - reject or roll back unsafe permission updates
---
 .../org/apache/hugegraph/driver/AuthManager.java   |   5 +
 .../org/apache/hugegraph/driver/HugeClient.java    |  33 ++
 .../hugegraph/driver/ServerCompatibility.java      |  21 +-
 .../driver/HugeClientCompatibilityTest.java        |  97 ++++++
 .../hugegraph/driver/ServerCompatibilityTest.java  |   6 +
 .../org/apache/hugegraph/unit/ManagerAPITest.java  |  15 +-
 .../controller/auth/GraphSpaceUserController.java  |  40 ++-
 .../controller/space/GraphSpaceController.java     |  30 +-
 .../controller/space/SchemaTemplateController.java |  28 +-
 .../apache/hugegraph/handler/LoginInterceptor.java |  26 +-
 .../hugegraph/service/auth/AuthContextService.java |  13 +-
 .../service/auth/GraphSpaceUserService.java        | 134 +++++--
 .../apache/hugegraph/service/auth/UserService.java | 216 ++++++++++--
 .../hugegraph/service/space/GraphSpaceService.java | 205 ++++++++---
 .../src/main/resources/i18n/messages.properties    |   6 +
 .../main/resources/i18n/messages_zh_CN.properties  |   6 +
 .../auth/GraphSpaceAuthOwnershipTest.java          |   9 +-
 .../controller/space/GraphSpaceControllerTest.java |  45 +++
 .../SchemaTemplateControllerSecurityTest.java      | 124 ++++++-
 .../service/auth/AuthContextServiceTest.java       |   6 +
 .../service/auth/GraphSpaceUserServiceTest.java    | 181 ++++++++++
 .../service/space/GraphSpaceServiceTest.java       | 162 ++++++++-
 .../apache/hugegraph/unit/AuthSecurityTest.java    |  16 +
 .../unit/UserServiceCompatibilityTest.java         | 383 +++++++++++++++++++--
 .../hubble-fe/src/api/auth-contract.test.js        |   8 +-
 hugegraph-hubble/hubble-fe/src/api/auth.js         |   4 +-
 .../src/api/request-error-semantics.test.js        |  47 +++
 hugegraph-hubble/hubble-fe/src/api/request.js      |   6 +
 .../src/components/GraphContextSwitcher/index.js   |  28 +-
 .../components/GraphContextSwitcher/index.test.js  |  30 +-
 .../hubble-fe/src/components/Topbar/index.ant.js   |  51 ++-
 .../components/Topbar/topbar-request-error.test.js |  49 ++-
 .../hubble-fe/src/pages/Account/EditLayer.js       |   8 +-
 .../hubble-fe/src/pages/Account/SpaceAccess.js     |  53 ++-
 .../src/pages/Account/SpaceAccess.test.js          |  92 ++++-
 .../pages/Account/account-edit-recovery.test.js    | 138 +++++++-
 .../hubble-fe/src/pages/Account/pagedRecords.js    |  74 ++++
 hugegraph-hubble/hubble-fe/src/pages/My/index.js   |  37 +-
 .../hubble-fe/src/pages/My/my-recovery.test.js     |  30 ++
 hugegraph-hubble/hubble-fe/src/utils/user.js       |  14 +
 40 files changed, 2193 insertions(+), 283 deletions(-)

diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java
index af69f518b..f8cc35216 100644
--- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java
+++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/AuthManager.java
@@ -385,6 +385,11 @@ public class AuthManager {
                    .checkPermission(HugePermission.SPACE, graphSpace);
     }
 
+    public boolean isSpaceMember(String graphSpace) {
+        return this.managerAPI(graphSpace)
+                   .checkPermission(HugePermission.SPACE_MEMBER, graphSpace);
+    }
+
     public boolean checkDefaultRole(String graphSpace, String role) {
         return this.managerAPI(graphSpace)
                    .checkDefaultRole(graphSpace, role, "");
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 41cf3e447..c314405f9 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
@@ -22,9 +22,12 @@ import java.io.Closeable;
 import lombok.Getter;
 
 import org.apache.hugegraph.client.RestClient;
+import org.apache.hugegraph.exception.ServerException;
 import org.apache.hugegraph.rest.ClientException;
 import org.apache.hugegraph.rest.RestClientConfig;
+import org.apache.hugegraph.structure.auth.TokenPayload;
 import org.apache.hugegraph.structure.auth.User;
+import org.apache.hugegraph.util.E;
 import org.apache.hugegraph.util.VersionUtil;
 import org.apache.hugegraph.version.ClientVersion;
 import org.slf4j.Logger;
@@ -265,6 +268,10 @@ public class HugeClient implements Closeable {
         return this.compatibility.supportsDefaultRole();
     }
 
+    public boolean supportsPersonalProfileUpdate() {
+        return this.compatibility.supportsPersonalProfileUpdate();
+    }
+
     public User findUserByName(String name) {
         if (this.supportsDefaultRole()) {
             return this.auth.getUserByName(name);
@@ -275,6 +282,32 @@ public class HugeClient implements Closeable {
                         .orElse(null);
     }
 
+    public User findCurrentUser(String expectedUsername) {
+        TokenPayload payload = this.auth.verifyToken();
+        E.checkState(payload != null &&
+                     !Strings.isNullOrEmpty(payload.userId()) &&
+                     !Strings.isNullOrEmpty(payload.username()),
+                     "Invalid current-user identity");
+        E.checkState(payload.username().equals(expectedUsername),
+                     "Authenticated user does not match the expected user");
+
+        User user;
+        try {
+            user = this.auth.getUser(payload.userId());
+        } catch (ServerException e) {
+            if (e.status() != 403 ||
+                this.supportsPersonalProfileUpdate()) {
+                throw e;
+            }
+            user = new User();
+            user.setId(payload.userId());
+            user.name(payload.username());
+        }
+        E.checkState(user != null && expectedUsername.equals(user.name()),
+                     "Current-user record does not match the token identity");
+        return user;
+    }
+
     public MetricsManager metrics() {
         return this.metrics;
     }
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
index b695edfe8..ac329aacf 100644
--- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java
+++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java
@@ -74,17 +74,26 @@ public final class ServerCompatibility {
         return profile(coreVersion, apiVersion).supportsDefaultRole();
     }
 
+    public static boolean supportsPersonalProfileUpdate(
+            String coreVersion, String apiVersion) {
+        return profile(coreVersion, apiVersion)
+               .supportsPersonalProfileUpdate();
+    }
+
     public enum Profile {
-        LEGACY(false, false),
-        GRAPHSPACE(true, false),
-        MODERN(true, true);
+        LEGACY(false, false, false),
+        GRAPHSPACE(true, false, false),
+        MODERN(true, true, true);
 
         private final boolean graphSpace;
         private final boolean defaultRole;
+        private final boolean personalProfileUpdate;
 
-        Profile(boolean graphSpace, boolean defaultRole) {
+        Profile(boolean graphSpace, boolean defaultRole,
+                boolean personalProfileUpdate) {
             this.graphSpace = graphSpace;
             this.defaultRole = defaultRole;
+            this.personalProfileUpdate = personalProfileUpdate;
         }
 
         public boolean supportsGraphSpace() {
@@ -94,5 +103,9 @@ public final class ServerCompatibility {
         public boolean supportsDefaultRole() {
             return this.defaultRole;
         }
+
+        public boolean supportsPersonalProfileUpdate() {
+            return this.personalProfileUpdate;
+        }
     }
 }
diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java
index aed9bb494..6e14e4ecf 100644
--- a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java
+++ b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java
@@ -19,6 +19,8 @@ package org.apache.hugegraph.driver;
 
 import java.util.Arrays;
 
+import org.apache.hugegraph.exception.ServerException;
+import org.apache.hugegraph.structure.auth.TokenPayload;
 import org.apache.hugegraph.structure.auth.User;
 import org.apache.hugegraph.testutil.Whitebox;
 import org.junit.Assert;
@@ -69,6 +71,101 @@ public class HugeClientCompatibilityTest {
                .getUserByName(Mockito.anyString());
     }
 
+    @Test
+    public void shouldFindCurrentUserFromVerifiedTokenIdentity() {
+        TokenPayload payload = Mockito.mock(TokenPayload.class);
+        User alice = user("alice");
+        Mockito.when(payload.userId()).thenReturn("user-id");
+        Mockito.when(payload.username()).thenReturn("alice");
+        Mockito.when(this.auth.verifyToken()).thenReturn(payload);
+        Mockito.when(this.auth.getUser("user-id")).thenReturn(alice);
+
+        Assert.assertSame(alice, this.client.findCurrentUser("alice"));
+        Mockito.verify(this.auth, Mockito.never()).listUsers();
+        Mockito.verify(this.auth, Mockito.never())
+               .getUserByName(Mockito.anyString());
+    }
+
+    @Test
+    public void shouldUseVerifiedIdentityWhenLegacySelfReadIsForbidden() {
+        TokenPayload payload = Mockito.mock(TokenPayload.class);
+        ServerException forbidden = new ServerException("forbidden");
+        forbidden.status(403);
+        Whitebox.setInternalState(
+                this.client, "compatibility",
+                ServerCompatibility.Profile.GRAPHSPACE);
+        Mockito.when(payload.userId()).thenReturn("user-id");
+        Mockito.when(payload.username()).thenReturn("alice");
+        Mockito.when(this.auth.verifyToken()).thenReturn(payload);
+        Mockito.when(this.auth.getUser("user-id")).thenThrow(forbidden);
+
+        User user = this.client.findCurrentUser("alice");
+
+        Assert.assertEquals("user-id", user.id());
+        Assert.assertEquals("alice", user.name());
+        Mockito.verify(this.auth, Mockito.never()).listUsers();
+    }
+
+    @Test
+    public void shouldNotHideForbiddenModernSelfRead() {
+        TokenPayload payload = Mockito.mock(TokenPayload.class);
+        ServerException forbidden = new ServerException("forbidden");
+        forbidden.status(403);
+        Whitebox.setInternalState(
+                this.client, "compatibility",
+                ServerCompatibility.Profile.MODERN);
+        Mockito.when(payload.userId()).thenReturn("user-id");
+        Mockito.when(payload.username()).thenReturn("alice");
+        Mockito.when(this.auth.verifyToken()).thenReturn(payload);
+        Mockito.when(this.auth.getUser("user-id")).thenThrow(forbidden);
+
+        try {
+            this.client.findCurrentUser("alice");
+            Assert.fail("Expected modern self-read failure");
+        } catch (ServerException ignored) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void shouldNotUseLegacyFallbackForInvalidOrMissingUser() {
+        TokenPayload payload = Mockito.mock(TokenPayload.class);
+        Whitebox.setInternalState(
+                this.client, "compatibility",
+                ServerCompatibility.Profile.GRAPHSPACE);
+        Mockito.when(payload.userId()).thenReturn("user-id");
+        Mockito.when(payload.username()).thenReturn("alice");
+        Mockito.when(this.auth.verifyToken()).thenReturn(payload);
+
+        for (int status : Arrays.asList(401, 404)) {
+            ServerException failure = new ServerException("failure");
+            failure.status(status);
+            Mockito.doThrow(failure).when(this.auth).getUser("user-id");
+            try {
+                this.client.findCurrentUser("alice");
+                Assert.fail("Expected legacy self-read failure");
+            } catch (ServerException actual) {
+                Assert.assertSame(failure, actual);
+            }
+        }
+    }
+
+    @Test
+    public void shouldRejectMismatchedCurrentUserIdentity() {
+        TokenPayload payload = Mockito.mock(TokenPayload.class);
+        Mockito.when(payload.userId()).thenReturn("user-id");
+        Mockito.when(payload.username()).thenReturn("bob");
+        Mockito.when(this.auth.verifyToken()).thenReturn(payload);
+
+        try {
+            this.client.findCurrentUser("alice");
+            Assert.fail("Expected a mismatched current-user identity");
+        } catch (IllegalStateException ignored) {
+            // Expected
+        }
+        Mockito.verify(this.auth, Mockito.never()).getUser(Mockito.any());
+    }
+
     private static User user(String name) {
         User user = new User();
         user.name(name);
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
index 05ebb41dd..577071974 100644
--- a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java
+++ b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java
@@ -41,6 +41,12 @@ public class ServerCompatibilityTest {
                           "1.7.0", "0.72.0.0"));
         Assert.assertFalse(ServerCompatibility.supportsDefaultRole(
                            "1.8.0", "0.71.0.0"));
+        Assert.assertFalse(
+                ServerCompatibility.supportsPersonalProfileUpdate(
+                        "1.7.0", "0.71.0.0"));
+        Assert.assertTrue(
+                ServerCompatibility.supportsPersonalProfileUpdate(
+                        "1.8.0", "0.72.0.0"));
         Assert.assertEquals(ServerCompatibility.Profile.GRAPHSPACE,
                             ServerCompatibility.profile("1.7.1",
                                                         "0.71.0.0"));
diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/ManagerAPITest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/ManagerAPITest.java
index 7fdf1aebd..e98ebcb67 100644
--- a/hugegraph-client/src/test/java/org/apache/hugegraph/unit/ManagerAPITest.java
+++ b/hugegraph-client/src/test/java/org/apache/hugegraph/unit/ManagerAPITest.java
@@ -146,21 +146,28 @@ public class ManagerAPITest extends BaseUnitTest {
 
         AuthManager auth = new AuthManager(client, "DEFAULT", null);
         Assert.assertTrue(auth.isSpaceAdmin("space_a"));
-        Assert.assertTrue(auth.checkDefaultRole("space_b", "analyst"));
+        Assert.assertTrue(auth.isSpaceMember("space_b"));
+        Assert.assertTrue(auth.checkDefaultRole("space_c", "analyst"));
 
         Assert.assertEquals("graphspaces/space_a/auth/managers/check",
                             path.getAllValues().get(0));
-        Assert.assertEquals("graphspaces/space_b/auth/managers/default",
+        Assert.assertEquals("graphspaces/space_b/auth/managers/check",
                             path.getAllValues().get(1));
+        Assert.assertEquals("graphspaces/space_c/auth/managers/default",
+                            path.getAllValues().get(2));
         Assert.assertEquals(HugePermission.SPACE,
                             params.getAllValues().get(0).get("type"));
+        Assert.assertEquals(HugePermission.SPACE_MEMBER,
+                            params.getAllValues().get(1).get("type"));
         Assert.assertEquals("space_a",
                             params.getAllValues().get(0).get("graphspace"));
         Assert.assertEquals("space_b",
                             params.getAllValues().get(1).get("graphspace"));
+        Assert.assertEquals("space_c",
+                            params.getAllValues().get(2).get("graphspace"));
         Assert.assertEquals("analyst",
-                            params.getAllValues().get(1).get("role"));
-        Assert.assertFalse(params.getAllValues().get(1).containsKey("graph"));
+                            params.getAllValues().get(2).get("role"));
+        Assert.assertFalse(params.getAllValues().get(2).containsKey("graph"));
     }
 
     @Test
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java
index 03dd4fb70..5302227f0 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java
@@ -27,6 +27,7 @@ import org.apache.hugegraph.entity.auth.UserView;
 import org.apache.hugegraph.service.auth.GraphSpaceUserService;
 import org.apache.hugegraph.structure.auth.User;
 import org.apache.hugegraph.structure.auth.UserManager;
+import org.apache.hugegraph.util.E;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -86,18 +87,25 @@ public class GraphSpaceUserController extends AuthController {
             @PathVariable("id") String userId) {
         HugeClient client =
                 this.requireGraphSpaceAuthorizationAdmin(graphSpace);
-        return client.auth().addSpaceAdmin(userId, graphSpace);
+        User account = client.auth().getUser(userId);
+        E.checkNotNull(account, "User");
+        return client.auth().addSpaceAdmin(account.name(), graphSpace);
     }
 
     @PutMapping("{id}/preset")
     public void setPermissionPreset(
             @PathVariable("graphspace") String graphSpace,
-            @PathVariable("id") String userId,
+            @PathVariable("id") String identity,
             @RequestBody Map<String, String> body) {
         String preset = body.get("permission_preset");
-        HugeClient client = this.requirePresetManager(graphSpace, userId,
+        String userId = body.get("user_id");
+        String username = body.get("username");
+        HugeClient client = this.requirePresetManager(graphSpace, username,
                                                       preset);
-        this.userService.applySpacePreset(client, graphSpace, userId, preset);
+        E.checkArgument(identity.equals(userId) ||
+                        identity.equals(username),
+                        "The account identity in the path and body must match");
+        this.userService.applySpacePreset(client, graphSpace, userId, username, preset);
     }
 
     @DeleteMapping("spaceadmin/{id}")
@@ -106,7 +114,9 @@ public class GraphSpaceUserController extends AuthController {
             @PathVariable("id") String userId) {
         HugeClient client =
                 this.requireGraphSpaceAuthorizationAdmin(graphSpace);
-        client.auth().delSpaceAdmin(userId, graphSpace);
+        User account = client.auth().getUser(userId);
+        E.checkNotNull(account, "User");
+        client.auth().delSpaceAdmin(account.name(), graphSpace);
     }
 
     @PostMapping
@@ -134,19 +144,31 @@ public class GraphSpaceUserController extends AuthController {
         this.userService.unauthUser(client, graphSpace, userId);
     }
 
-    private HugeClient requirePresetManager(String graphSpace, String userId,
+    private HugeClient requirePresetManager(String graphSpace, String username,
                                             String preset) {
         if ("GS_ADMIN".equals(preset)) {
             return this.requireGraphSpaceAuthorizationAdmin(graphSpace);
         }
-        return this.requireMemberManager(graphSpace, userId);
+        return this.requireMemberManagerByUsername(graphSpace, username);
     }
 
     private HugeClient requireMemberManager(String graphSpace,
                                             String userId) {
         HugeClient client = this.requireGraphSpaceManager(graphSpace);
-        if (client.auth().listSuperAdmin().contains(userId) ||
-            client.auth().listSpaceAdmin(graphSpace).contains(userId)) {
+        User account = client.auth().getUser(userId);
+        E.checkNotNull(account, "User");
+        return this.requireMemberManagerByUsername(client, graphSpace, account.name());
+    }
+
+    private HugeClient requireMemberManagerByUsername(String graphSpace, String username) {
+        HugeClient client = this.requireGraphSpaceManager(graphSpace);
+        return this.requireMemberManagerByUsername(client, graphSpace, username);
+    }
+
+    private HugeClient requireMemberManagerByUsername(HugeClient client, String graphSpace, String username) {
+        E.checkArgument(username != null && !username.isEmpty(), "The account name can't be empty");
+        if (client.auth().listSuperAdmin().contains(username) ||
+            client.auth().listSpaceAdmin(graphSpace).contains(username)) {
             return this.requireGraphSpaceAuthorizationAdmin(graphSpace);
         }
         return client;
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/GraphSpaceController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/GraphSpaceController.java
index fb1d8900a..e3a8e2d6a 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/GraphSpaceController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/GraphSpaceController.java
@@ -34,7 +34,6 @@ import org.apache.hugegraph.service.auth.UserService;
 import org.apache.hugegraph.service.graphs.GraphsService;
 import org.apache.hugegraph.service.space.GraphSpaceService;
 import org.apache.hugegraph.util.E;
-import org.apache.hugegraph.util.PageUtil;
 import org.apache.hugegraph.util.UrlUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.DeleteMapping;
@@ -49,7 +48,6 @@ import org.springframework.web.bind.annotation.RestController;
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 @RestController
 @RequestMapping(Constant.API_VERSION + "graphspaces")
@@ -82,8 +80,11 @@ public class GraphSpaceController extends BaseController {
                                    Collections.singletonList("DEFAULT"));
         }
 
+        HugeClient client = this.authClient(null, null);
         List<String> graphSpaces =
-                this.graphSpaceService.listAll(this.authClient(null, null));
+                this.authMode != null && this.authMode.anonymous() ?
+                this.graphSpaceService.listAnonymous(client) :
+                this.graphSpaceService.listAll(client);
         return ImmutableMap.of("graphspaces", graphSpaces);
     }
 
@@ -104,8 +105,11 @@ public class GraphSpaceController extends BaseController {
         }
         if (this.authMode != null && this.authMode.anonymous()) {
             HugeClient client = this.authClient(null, null);
-            List<Map<String, Object>> graphSpaces = this.graphSpaceService.queryAnonymousGs(client, query, createTime);
-            return all ? graphSpaces : PageUtil.page(graphSpaces, pageNo, pageSize);
+            return all ?
+                   this.graphSpaceService.queryAnonymousGs(client, query,
+                                                           createTime) :
+                   this.graphSpaceService.queryAnonymousGsPage(
+                           client, query, createTime, pageNo, pageSize);
         }
         if (all) {
             HugeClient client = this.authClient(null, null);
@@ -119,8 +123,8 @@ public class GraphSpaceController extends BaseController {
         return this.userService.isSuperAdmin(client) ?
                graphSpaceService.queryPage(client, query, createTime,
                                            pageNo, pageSize) :
-               PageUtil.page(graphSpaceService.queryAccessibleGs(
-                       client, query, createTime), pageNo, pageSize);
+               graphSpaceService.queryAccessibleGsPage(
+                       client, query, createTime, pageNo, pageSize);
     }
 
     @GetMapping("{graphspace}/auth")
@@ -128,8 +132,11 @@ public class GraphSpaceController extends BaseController {
         if (!isPdEnabled()) {
             return ImmutableMap.of("auth", false);
         }
-        boolean isAuth = graphSpaceService.isAuth(this.authClient(null, null),
-                                                  graphSpace);
+        HugeClient client = this.authClient(null, null);
+        boolean isAuth =
+                this.authMode != null && this.authMode.anonymous() ?
+                this.graphSpaceService.isAuthForAnonymous(client, graphSpace) :
+                this.graphSpaceService.isAuth(client, graphSpace);
         return ImmutableMap.of("auth", isAuth);
     }
 
@@ -144,10 +151,7 @@ public class GraphSpaceController extends BaseController {
         }
         HugeClient client = this.authClient(null, null);
         if (this.authMode != null && this.authMode.anonymous()) {
-            GraphSpaceEntity entity = GraphSpaceEntity.fromGraphSpace(this.graphSpaceService.getWithoutAdmins(client,
-                                                            graphspace));
-            entity.setStatistic(this.graphSpaceService.evCount(client, graphspace));
-            return this.graphSpaceService.toView(entity);
+            return this.graphSpaceService.getAnonymous(client, graphspace);
         }
         // Get GraphSpace Info
         return graphSpaceService.toView(
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/SchemaTemplateController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/SchemaTemplateController.java
index 7526183b5..f24182121 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/SchemaTemplateController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/SchemaTemplateController.java
@@ -19,6 +19,8 @@
 package org.apache.hugegraph.controller.space;
 
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 
 import com.google.common.collect.ImmutableMap;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -36,6 +38,7 @@ import org.apache.hugegraph.common.Constant;
 import org.apache.hugegraph.config.HugeConfig;
 import org.apache.hugegraph.controller.BaseController;
 import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.exception.ForbiddenException;
 import org.apache.hugegraph.options.HubbleOptions;
 import org.apache.hugegraph.service.space.SchemaTemplateService;
 import org.apache.hugegraph.structure.space.SchemaTemplate;
@@ -102,7 +105,8 @@ public class SchemaTemplateController extends BaseController {
     @DeleteMapping("{name}")
     public void delete(@PathVariable("graphspace") String graphSpace,
                        @PathVariable("name") String name) {
-        HugeClient client = this.requireGraphSpaceWrite(graphSpace);
+        HugeClient client = this.requireTemplateOwnerOrManager(graphSpace,
+                                                               name);
         schemaTemplateService.delete(client, name);
     }
 
@@ -110,8 +114,28 @@ public class SchemaTemplateController extends BaseController {
     public Object update(@PathVariable("graphspace") String graphSpace,
                          @PathVariable("name") String name,
                          @RequestBody SchemaTemplate schemaTemplate) {
-        HugeClient client = this.requireGraphSpaceWrite(graphSpace);
+        HugeClient client = this.requireTemplateOwnerOrManager(graphSpace,
+                                                               name);
         schemaTemplate.name(name);
         return schemaTemplateService.update(client, schemaTemplate);
     }
+
+    private HugeClient requireTemplateOwnerOrManager(String graphSpace,
+                                                     String name) {
+        HugeClient client = this.requireGraphSpaceWrite(graphSpace);
+        if (this.authMode != null && this.authMode.anonymous()) {
+            return client;
+        }
+        if (this.userService.isSuperAdmin(client) ||
+            this.userService.isAssignSpaceAdmin(client, graphSpace)) {
+            return client;
+        }
+
+        Map template = this.schemaTemplateService.get(client, name);
+        if (!Objects.equals(this.getUser(), template.get("creator"))) {
+            throw new ForbiddenException(
+                    "Permission denied: modify schema template");
+        }
+        return 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 5d49aa9f8..ae3baee1e 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
@@ -65,12 +65,32 @@ public class LoginInterceptor extends HandlerInterceptorAdapter {
     }
 
     private static boolean isAnonymousAuthManagement(String uri) {
-        if (uri.contains("/graphspaces/") && uri.contains("/auth/")) {
+        int apiIndex = uri.indexOf(Constant.API_VERSION);
+        if (apiIndex < 0) {
+            return false;
+        }
+        String apiPath = uri.substring(apiIndex);
+        String globalAuth = Constant.API_VERSION + "auth";
+        if (apiPath.equals(globalAuth + "/context") ||
+            apiPath.equals(globalAuth + "/status") ||
+            apiPath.equals(globalAuth + "/logout")) {
+            return false;
+        }
+        if (apiPath.equals(globalAuth) ||
+            apiPath.startsWith(globalAuth + "/")) {
             return true;
         }
-        if (!uri.contains("/auth/")) {
+
+        String graphSpaces = Constant.API_VERSION + "graphspaces/";
+        if (!apiPath.startsWith(graphSpaces)) {
+            return false;
+        }
+        int graphSpaceEnd = apiPath.indexOf('/', graphSpaces.length());
+        if (graphSpaceEnd < 0) {
             return false;
         }
-        return !uri.endsWith("/auth/context") && !uri.endsWith("/auth/status") && !uri.endsWith("/auth/logout");
+        String scopedAuth = apiPath.substring(0, graphSpaceEnd) + "/auth";
+        return apiPath.equals(scopedAuth) ||
+               apiPath.startsWith(scopedAuth + "/");
     }
 }
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 c0e699172..80ad33a69 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
@@ -69,6 +69,8 @@ public class AuthContextService {
     private static final char[] HEX = "0123456789abcdef".toCharArray();
     private static final Set<String> SELF_ACTIONS = set(
             "read", "update", "change_password");
+    private static final Set<String> LEGACY_SELF_ACTIONS = set(
+            "read", "change_password");
     private static final Set<String> CRUD_ACTIONS = set(
             "read", "create", "update", "delete");
     private static final Set<String> MEMBER_ACTIONS = set(
@@ -116,6 +118,8 @@ public class AuthContextService {
 
         boolean permissionPresets = pdEnabled &&
                                     client.supportsDefaultRole();
+        boolean profileUpdate =
+                client.supportsPersonalProfileUpdate();
         List<String> writeGraphSpaces = pdEnabled ?
                                         writeGraphSpaces(user,
                                                          permissionPresets) :
@@ -123,7 +127,8 @@ public class AuthContextService {
         Set<String> capabilities = this.capabilities(pdEnabled, role,
                                                      permissionPresets);
         Map<String, Set<String>> actions = this.actions(pdEnabled, role,
-                                                       permissionPresets);
+                                                       permissionPresets,
+                                                       profileUpdate);
         Map<String, Object> scopes = this.scopes(pdEnabled, role,
                                                  adminGraphSpaces,
                                                  writeGraphSpaces);
@@ -224,12 +229,14 @@ public class AuthContextService {
     }
 
     private Map<String, Set<String>> actions(boolean pdEnabled, String role,
-                                             boolean permissionPresets) {
+                                             boolean permissionPresets,
+                                             boolean profileUpdate) {
         Map<String, Set<String>> actions = new LinkedHashMap<>();
         boolean superAdmin = SUPERADMIN.equals(role);
         boolean spaceManager = pdEnabled && permissionPresets &&
                                (superAdmin || SPACEADMIN.equals(role));
-        actions.put("account", SELF_ACTIONS);
+        actions.put("account", profileUpdate ?
+                               SELF_ACTIONS : LEGACY_SELF_ACTIONS);
         actions.put("accounts", superAdmin ? CRUD_ACTIONS : emptySet());
         actions.put("graphspaces", pdEnabled ?
                    (superAdmin ? CRUD_ACTIONS : set("read")) : emptySet());
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 7feeadedc..325b6e382 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
@@ -167,6 +167,21 @@ public class GraphSpaceUserService extends AuthService {
     public void applyPermissionPresets(HugeClient client, String username,
                                        List<Map<String, String>> permissions,
                                        String preset) {
+        this.applyPermissionPresets(client, username, permissions, preset,
+                                    true);
+    }
+
+    public void applyPermissionPresetsForNewAccount(
+            HugeClient client, String username,
+            List<Map<String, String>> permissions, String preset) {
+        this.applyPermissionPresets(client, username, permissions, preset,
+                                    false);
+    }
+
+    private void applyPermissionPresets(HugeClient client, String username,
+                                        List<Map<String, String>> permissions,
+                                        String preset,
+                                        boolean reconcileUnrequestedSpaces) {
         if (preset == null || "SUPER_ADMIN".equals(preset)) {
             return;
         }
@@ -184,16 +199,44 @@ public class GraphSpaceUserService extends AuthService {
                 desired.put(graphSpace, permissionPreset);
             }
         }
-        for (String graphSpace : client.graphSpace().listGraphSpace()) {
-            String desiredPreset = desired.get(graphSpace);
-            if (desiredPreset == null) {
-                this.unauthUser(client, graphSpace,
-                                account.id().toString());
-            } else {
-                this.applySpacePreset(client, graphSpace,
-                                      account.id().toString(),
-                                      desiredPreset);
+        if (!reconcileUnrequestedSpaces) {
+            desired.forEach((graphSpace, desiredPreset) ->
+                    this.applySpacePreset(client, graphSpace,
+                                          account.id().toString(),
+                                          account.name(),
+                                          desiredPreset));
+            return;
+        }
+        List<String> graphSpaces = client.graphSpace().listGraphSpace();
+        Map<String, SpacePresetState> previous =
+                new java.util.LinkedHashMap<>();
+        String userId = account.id().toString();
+        String accountName = account.name();
+        for (String graphSpace : graphSpaces) {
+            boolean member = client.auth().listSpaceMember(graphSpace)
+                                   .contains(accountName);
+            previous.put(graphSpace, this.capturePresetState(
+                    client, graphSpace, userId, accountName, member));
+        }
+        try {
+            for (String graphSpace : graphSpaces) {
+                String desiredPreset = desired.get(graphSpace);
+                if (desiredPreset == null) {
+                    this.unauthUser(client, graphSpace, userId);
+                } else {
+                    this.applySpacePreset(client, graphSpace, userId,
+                                          accountName,
+                                          desiredPreset);
+                }
+            }
+        } catch (RuntimeException error) {
+            for (int i = graphSpaces.size() - 1; i >= 0; i--) {
+                String graphSpace = graphSpaces.get(i);
+                this.restorePresetState(client, graphSpace, userId,
+                                        accountName, previous.get(graphSpace),
+                                        error);
             }
+            throw error;
         }
     }
 
@@ -204,10 +247,19 @@ public class GraphSpaceUserService extends AuthService {
             return;
         }
         requirePermissionPresets(client);
+        requirePermissionPreset(preset);
+        if (permissions == null || permissions.isEmpty()) {
+            throw new ParameterizedException(
+                      "auth.permission-preset.graphspace-required");
+        }
         Set<String> graphSpaces =
                 new java.util.HashSet<>(client.graphSpace().listGraphSpace());
         for (Map<String, String> permission :
                 permissions == null ? new ArrayList<Map<String, String>>() : permissions) {
+            if (permission == null) {
+                throw new ParameterizedException(
+                          "auth.permission-preset.entry-invalid");
+            }
             String graphSpace = permission.get("graphspace");
             String permissionPreset = permission.get("permission_preset");
             if (graphSpace == null || !graphSpaces.contains(graphSpace)) {
@@ -216,29 +268,38 @@ public class GraphSpaceUserService extends AuthService {
                           graphSpace);
             }
             requirePermissionPreset(permissionPreset);
+            if (!preset.equals(permissionPreset)) {
+                throw new ParameterizedException(
+                          "auth.permission-preset.mismatch",
+                          permissionPreset, preset);
+            }
         }
     }
 
-    public void applySpacePreset(HugeClient client, String graphSpace,
-                                 String userId, String preset) {
+    public void applySpacePreset(HugeClient client, String graphSpace, String userId, String username, String preset) {
         requirePermissionPreset(preset);
         requirePermissionPresets(client);
-        E.checkArgument(!client.auth().listSuperAdmin().contains(userId),
+        E.checkArgument(username != null && !username.isEmpty(), "The account name can't be empty");
+        E.checkArgument(!client.auth().listSuperAdmin().contains(username),
                         "Can't assign GraphSpace preset to super " +
-                        "administrator '%s'", userId);
+                        "administrator '%s'", username);
         boolean wasMember =
-                client.auth().listSpaceMember(graphSpace).contains(userId);
-        String username = userId;
+                client.auth().listSpaceMember(graphSpace).contains(username);
+        String resolvedUserId = userId;
         SpacePresetState previous = null;
         try {
             if (!wasMember) {
-                client.auth().addSpaceMember(userId, graphSpace);
+                client.auth().addSpaceMember(username, graphSpace);
             }
-            User account = client.auth().getUser(userId);
+            User account = userId == null ?
+                           client.findUserByName(username) :
+                           client.auth().getUser(userId);
             E.checkNotNull(account, "User");
-            username = account.name();
-            previous = this.capturePresetState(client, graphSpace, userId,
-                                               username, wasMember);
+            E.checkArgument(username.equals(account.name()),
+                            "Account id '%s' belongs to '%s', not '%s'",
+                            userId, account.name(), username);
+            resolvedUserId = account.id().toString();
+            previous = this.capturePresetState(client, graphSpace, resolvedUserId, username, wasMember);
             previous.customRoles.forEach(
                     belong -> this.belongService.deleteById(
                             client, graphSpace, belong.getId()));
@@ -265,11 +326,10 @@ public class GraphSpaceUserService extends AuthService {
             this.setDefaultRole(client, graphSpace, username, role);
         } catch (RuntimeException e) {
             if (previous == null) {
-                this.rollbackNewMember(client, graphSpace, userId,
+                this.rollbackNewMember(client, graphSpace, username,
                                        wasMember, e);
             } else {
-                this.restorePresetState(client, graphSpace, userId, username,
-                                        previous, e);
+                this.restorePresetState(client, graphSpace, resolvedUserId, username, previous, e);
             }
             throw e;
         }
@@ -308,6 +368,9 @@ public class GraphSpaceUserService extends AuthService {
                                     String userId, String username,
                                     SpacePresetState previous,
                                     RuntimeException failure) {
+        if (previous.member) {
+            this.rollbackNewMember(client, graphSpace, username, true, failure);
+        }
         previous.customRoles.forEach(belong -> {
             this.tryRestore(() -> {
                 Set<String> currentRoles = this.belongService.list(
@@ -336,8 +399,9 @@ public class GraphSpaceUserService extends AuthService {
                 client.auth().delSpaceAdmin(username, graphSpace);
             }
         }, graphSpace, userId, "administrator", failure);
-        this.rollbackNewMember(client, graphSpace, userId,
-                               previous.member, failure);
+        if (!previous.member) {
+            this.rollbackNewMember(client, graphSpace, username, false, failure);
+        }
     }
 
     private void restoreDefaultRole(HugeClient client, String graphSpace,
@@ -353,17 +417,17 @@ public class GraphSpaceUserService extends AuthService {
     }
 
     private void rollbackNewMember(HugeClient client, String graphSpace,
-                                   String userId, boolean expected,
+                                   String username, boolean expected,
                                    RuntimeException failure) {
         this.tryRestore(() -> {
             boolean current = client.auth().listSpaceMember(graphSpace)
-                                    .contains(userId);
+                                    .contains(username);
             if (expected && !current) {
-                client.auth().addSpaceMember(userId, graphSpace);
+                client.auth().addSpaceMember(username, graphSpace);
             } else if (!expected && current) {
-                client.auth().delSpaceMember(userId, graphSpace);
+                client.auth().delSpaceMember(username, graphSpace);
             }
-        }, graphSpace, userId, "membership", failure);
+        }, graphSpace, username, "membership", failure);
     }
 
     private void tryRestore(Runnable action, String graphSpace,
@@ -386,14 +450,18 @@ public class GraphSpaceUserService extends AuthService {
     }
 
     private static void requirePermissionPreset(String preset) {
-        if (!"GS_READ_ONLY".equals(preset) &&
-            !"GS_READ_WRITE".equals(preset) &&
-            !"GS_ADMIN".equals(preset)) {
+        if (!isGraphSpacePreset(preset)) {
             throw new ParameterizedException(
                       "auth.permission-preset.invalid", preset);
         }
     }
 
+    private static boolean isGraphSpacePreset(String preset) {
+        return "GS_READ_ONLY".equals(preset) ||
+               "GS_READ_WRITE".equals(preset) ||
+               "GS_ADMIN".equals(preset);
+    }
+
     public boolean hasCustomRoles(HugeClient client, String graphSpace,
                                   String userId) {
         return !this.belongService.list(client, graphSpace, null, userId).isEmpty();
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 003647598..0d44836db 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
@@ -42,7 +42,9 @@ import org.apache.hugegraph.driver.AuthManager;
 import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.entity.auth.UserEntity;
 import org.apache.hugegraph.exception.InternalException;
+import org.apache.hugegraph.exception.ParameterizedException;
 import org.apache.hugegraph.exception.ServerException;
+import org.apache.hugegraph.exception.UnauthorizedException;
 import org.apache.hugegraph.structure.auth.User;
 import org.apache.hugegraph.util.E;
 import org.apache.hugegraph.util.HubbleUtil;
@@ -183,7 +185,7 @@ public class UserService extends AuthService {
     }
 
     public UserEntity getpersonal(HugeClient hugeClient, String username) {
-        User user = hugeClient.findUserByName(username);
+        User user = currentUser(hugeClient, username);
         if (user == null) {
             throw new InternalException("auth.user.get.%s Not Exits",
                     username);
@@ -217,10 +219,10 @@ public class UserService extends AuthService {
     }
 
     public void add(HugeClient client, UserEntity ue) {
-        if (isPdEnabled() && client.supportsDefaultRole()) {
-            this.graphSpaceUserService.validatePermissionPresets(client, ue.getGraphspacePermissions(),
-                    ue.getPermissionPreset());
-        }
+        boolean permissionPresets = isPdEnabled() &&
+                                    client.supportsDefaultRole();
+        this.validatePermissionMutation(client, ue, true,
+                                        permissionPresets);
         User user = new User();
         user.name(ue.getName());
         user.password(ue.getPassword());
@@ -233,19 +235,60 @@ public class UserService extends AuthService {
         }
 
         User newUser = client.auth().createUser(user);
-        if (ue.getAdminSpaces() != null) {
-            for (String graphspace : ue.getAdminSpaces()) {
-                client.auth().addSpaceAdmin(ue.getName(), graphspace);
+        List<String> attemptedAdminSpaces = new ArrayList<>();
+        boolean superAdminAttempted = false;
+        try {
+            if (!permissionPresets && ue.getAdminSpaces() != null) {
+                for (String graphspace : ue.getAdminSpaces()) {
+                    attemptedAdminSpaces.add(graphspace);
+                    client.auth().addSpaceAdmin(ue.getName(), graphspace);
+                }
+            }
+            if (permissionPresets) {
+                this.graphSpaceUserService
+                    .applyPermissionPresetsForNewAccount(
+                        client, ue.getName(),
+                        ue.getGraphspacePermissions(),
+                        ue.getPermissionPreset());
             }
+            if (newUser != null && ue.isSuperadmin()) {
+                superAdminAttempted = true;
+                client.auth().addSuperAdmin(ue.getName());
+            }
+        } catch (RuntimeException error) {
+            this.rollbackNewAccount(client, newUser, ue.getName(),
+                                    attemptedAdminSpaces,
+                                    superAdminAttempted, error);
+            throw error;
         }
-        if (isPdEnabled() && client.supportsDefaultRole()) {
-            this.graphSpaceUserService.applyPermissionPresets(client, ue.getName(), ue.getGraphspacePermissions(),
-                    ue.getPermissionPreset());
+    }
+
+    private void rollbackNewAccount(HugeClient client, User user,
+                                    String username,
+                                    List<String> attemptedAdminSpaces,
+                                    boolean superAdminAttempted,
+                                    RuntimeException failure) {
+        if (superAdminAttempted) {
+            this.suppressRollback(
+                    () -> client.auth().delSuperAdmin(username), failure);
         }
+        for (int i = attemptedAdminSpaces.size() - 1; i >= 0; i--) {
+            String graphSpace = attemptedAdminSpaces.get(i);
+            this.suppressRollback(
+                    () -> client.auth().delSpaceAdmin(username, graphSpace),
+                    failure);
+        }
+        if (user != null) {
+            this.suppressRollback(
+                    () -> client.auth().deleteUser(user.id()), failure);
+        }
+    }
 
-        if (newUser != null && ue.isSuperadmin()) {
-            // add superadmin
-            client.auth().addSuperAdmin(newUser.id().toString());
+    private void suppressRollback(Runnable rollback, RuntimeException failure) {
+        try {
+            rollback.run();
+        } catch (RuntimeException rollbackError) {
+            failure.addSuppressed(rollbackError);
         }
     }
 
@@ -488,11 +531,13 @@ public class UserService extends AuthService {
 
     private static boolean hasCurrentUserAccess(HugeClient client,
                                                 String graphSpace) {
+        if (!client.supportsDefaultRole()) {
+            return client.auth().isSpaceMember(graphSpace);
+        }
         if (client.auth().checkDefaultRole(graphSpace, "analyst")) {
             return true;
         }
-        return client.supportsDefaultRole() &&
-               hasCurrentUserObserverRole(client, graphSpace);
+        return hasCurrentUserObserverRole(client, graphSpace);
     }
 
     private static boolean hasCurrentUserObserverRole(HugeClient client,
@@ -540,10 +585,11 @@ public class UserService extends AuthService {
     }
 
     public void update(HugeClient hugeClient, UserEntity userEntity) {
-        if (isPdEnabled() && hugeClient.supportsDefaultRole()) {
-            this.graphSpaceUserService.validatePermissionPresets(hugeClient, userEntity.getGraphspacePermissions(),
-                    userEntity.getPermissionPreset());
-        }
+        boolean permissionPresets = isPdEnabled() &&
+                                    hugeClient.supportsDefaultRole();
+        boolean permissionMutation = userEntity.getPermissionPreset() != null;
+        this.validatePermissionMutation(hugeClient, userEntity, false,
+                                        permissionPresets);
         User user = new User();
         user.setId(userEntity.getId());
         user.name(userEntity.getName());
@@ -554,28 +600,115 @@ public class UserService extends AuthService {
         if (isPdEnabled()) {
             user.nickname(userEntity.getNickname());
         }
-        updateAdminSpace(hugeClient, userEntity.getName(), userEntity.getAdminSpaces());
-
-        // 设置超级管理员权限
-        boolean curSuperAdmin = isSuperAdmin(hugeClient, user.id().toString());
-        if (curSuperAdmin && !userEntity.isSuperadmin()) {
-            hugeClient.auth().delSuperAdmin(user.id().toString());
+        if (permissionPresets && permissionMutation) {
+            this.updateModernPermissionAccount(hugeClient, user, userEntity);
+            return;
         }
-        if (!curSuperAdmin && userEntity.isSuperadmin()) {
-            hugeClient.auth().addSuperAdmin(user.id().toString());
+        if (!permissionPresets) {
+            updateAdminSpace(hugeClient, userEntity.getName(),
+                             userEntity.getAdminSpaces());
+        }
+        if (!permissionPresets) {
+            String username = userEntity.getName();
+            boolean currentSuperAdmin =
+                    isSuperAdmin(hugeClient, user.id().toString());
+            if (currentSuperAdmin && !userEntity.isSuperadmin()) {
+                hugeClient.auth().delSuperAdmin(username);
+            }
+            if (!currentSuperAdmin && userEntity.isSuperadmin()) {
+                hugeClient.auth().addSuperAdmin(username);
+            }
         }
 
         hugeClient.auth().updateUser(user);
-        if (isPdEnabled() && hugeClient.supportsDefaultRole()) {
-            this.graphSpaceUserService.applyPermissionPresets(hugeClient, userEntity.getName(),
+    }
+
+    private void updateModernPermissionAccount(HugeClient client, User user,
+                                               UserEntity userEntity) {
+        String userId = user.id().toString();
+        User previous = client.auth().getUser(userId);
+        E.checkNotNull(previous, "User");
+        String username = previous.name();
+        boolean previousSuperAdmin = client.auth().listSuperAdmin().contains(username);
+        try {
+            if (previousSuperAdmin && !userEntity.isSuperadmin()) {
+                client.auth().delSuperAdmin(username);
+            }
+            if (!previousSuperAdmin && userEntity.isSuperadmin()) {
+                client.auth().addSuperAdmin(username);
+            }
+            client.auth().updateUser(user);
+            this.graphSpaceUserService.applyPermissionPresets(
+                    client, userEntity.getName(),
                     userEntity.getGraphspacePermissions(),
                     userEntity.getPermissionPreset());
+        } catch (RuntimeException error) {
+            this.restoreAccountProfile(client, previous, error);
+            this.restoreSuperAdmin(client, username, previousSuperAdmin, error);
+            throw error;
         }
     }
 
+    private void restoreAccountProfile(HugeClient client, User previous,
+                                       RuntimeException failure) {
+        try {
+            client.auth().updateUser(previous);
+        } catch (RuntimeException rollbackError) {
+            failure.addSuppressed(rollbackError);
+            log.warn("Failed to restore profile for account '{}'",
+                     previous.id(), rollbackError);
+        }
+    }
+
+    private void restoreSuperAdmin(HugeClient client, String username,
+                                   boolean expected,
+                                   RuntimeException failure) {
+        try {
+            boolean current = client.auth().listSuperAdmin()
+                                    .contains(username);
+            if (expected && !current) {
+                client.auth().addSuperAdmin(username);
+            } else if (!expected && current) {
+                client.auth().delSuperAdmin(username);
+            }
+        } catch (RuntimeException rollbackError) {
+            failure.addSuppressed(rollbackError);
+            log.warn("Failed to restore super administrator state for '{}'",
+                     username, rollbackError);
+        }
+    }
+
+    private void validatePermissionMutation(HugeClient client,
+                                            UserEntity user,
+                                            boolean create,
+                                            boolean supported) {
+        if (!supported) {
+            return;
+        }
+        String preset = user.getPermissionPreset();
+        if (preset == null) {
+            if (create) {
+                throw new ParameterizedException(
+                          "auth.permission-preset.account-required");
+            }
+            return;
+        }
+        boolean superAdminPreset = "SUPER_ADMIN".equals(preset);
+        if (superAdminPreset != user.isSuperadmin()) {
+            throw new ParameterizedException(
+                      "auth.permission-preset.superadmin-mismatch");
+        }
+        this.graphSpaceUserService.validatePermissionPresets(
+                client, user.getGraphspacePermissions(), preset);
+    }
+
     public void updatePersonal(HugeClient hugeClient, String username,
                                String nickname, String description) {
-        User user = hugeClient.findUserByName(username);
+        if (!hugeClient.supportsPersonalProfileUpdate()) {
+            throw new ParameterizedException(
+                      "auth.profile.update-unsupported");
+        }
+        User user = currentUser(hugeClient, username);
         if (isPdEnabled()) {
             user.nickname(nickname);
         } else {
@@ -602,7 +735,7 @@ public class UserService extends AuthService {
         }
         // Must fetch user first to get the ID, otherwise updateUser sends
         // PUT to the collection path (no {id}) and gets HTTP 405.
-        User user = hugeClient.findUserByName(username);
+        User user = currentUser(hugeClient, username);
         user.password(newpwd);
         hugeClient.auth().updateUser(user);
         return Response.builder()
@@ -610,6 +743,19 @@ public class UserService extends AuthService {
                 .build();
     }
 
+    private static User currentUser(HugeClient client, String username) {
+        try {
+            return client.findCurrentUser(username);
+        } catch (IllegalStateException e) {
+            throw new UnauthorizedException();
+        } catch (ServerException e) {
+            if (e.status() == 401 || e.status() == 403 || e.status() == 404) {
+                throw new UnauthorizedException();
+            }
+            throw e;
+        }
+    }
+
     public List<String> listAdminSpace(HugeClient hugeClient, String username) {
         if (!isPdEnabled()) {
             return new ArrayList<>();
@@ -652,6 +798,7 @@ public class UserService extends AuthService {
         for (String adminspace : adminspaces) {
             if (!oldadminspaces.contains(adminspace)) {
                 this.graphSpaceUserService.applySpacePreset(hugeClient, adminspace, account.id().toString(),
+                        account.name(),
                         "GS_ADMIN");
             }
         }
@@ -694,9 +841,8 @@ public class UserService extends AuthService {
         if (!isPdEnabled()) {
             return false;
         }
-        // Only used by superadmin
-        // Check: if user is spaceadmin for any graphspace
-        return client.auth().listSuperAdmin().contains(uid);
+        User account = client.auth().getUser(uid);
+        return account != null && client.auth().listSuperAdmin().contains(account.name());
     }
 
     public boolean isSuperAdmin(HugeClient client) {
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
index 10e5d6a58..2b5afc2dc 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/space/GraphSpaceService.java
@@ -19,13 +19,16 @@
 package org.apache.hugegraph.service.space;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.apache.hugegraph.api.task.TasksWithPage;
 // TODO fix import
 //import org.apache.hugegraph.client.api.task.TasksWithPage;
 import org.apache.hugegraph.common.Constant;
 import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.entity.space.GraphSpaceEntity;
+import org.apache.hugegraph.exception.ExternalException;
 import org.apache.hugegraph.exception.InternalException;
+import org.apache.hugegraph.exception.ServerException;
 import org.apache.hugegraph.service.auth.UserService;
 import org.apache.hugegraph.service.graphs.GraphsService;
 import org.apache.hugegraph.structure.Task;
@@ -38,6 +41,7 @@ import org.apache.hugegraph.util.Log;
 import org.apache.hugegraph.util.PageUtil;
 import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Service;
 
 import java.text.ParseException;
@@ -50,6 +54,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 @Service
@@ -116,39 +121,63 @@ public class GraphSpaceService {
     public IPage<Map<String, Object>> queryPage(HugeClient client, String query,
                                                 String createTime, int pageNo,
                                                 int pageSize) {
-        List<Map<String, Object>> results =
-                queryAllGs(client, query, createTime);
-        return PageUtil.page(results, pageNo, pageSize);
+        return pageAndMap(queryAllGsMetadata(client, query, createTime),
+                          pageNo, pageSize,
+                          info -> graphSpaceView(client, info));
     }
 
     public List<Map<String, Object>> queryAllGs(HugeClient client, String query,
                                                 String createTime) {
-        List<Map<String, Object>> results =
-                client.graphSpace().listProfile(query).stream()
-                      .filter((s) -> s.get("create_time").toString()
-                                      .compareTo(createTime) > 0)
-                      .collect(Collectors.toList());
-        // 将DEFAULT和neizhianli的图空间排在前面, 其他图空间按字母序排序
-        Collections.sort(results, (a, b) ->
-                new BuiltInFirst().compare(a.get("name").toString(),
-                                           b.get("name").toString()));
-        for (Map<String, Object> info : results) {
-            removeSensitiveFields(info);
-            String name = info.get("name").toString();
-            info.put("graphspace_admin",
-                     userService.listGraphSpaceAdmin(client, name));
-            Map<String, Object> statisticTotal = evCount(client, name);
-            info.put("statistic", statisticTotal);
-        }
+        return queryAllGsMetadata(client, query, createTime).stream()
+                .map(info -> graphSpaceView(client, info))
+                .collect(Collectors.toList());
+    }
+
+    private List<Map<String, Object>> queryAllGsMetadata(
+            HugeClient client, String query, String createTime) {
+        List<Map<String, Object>> results = client.graphSpace()
+                .listProfile(query).stream()
+                .filter(info -> info.get("create_time").toString()
+                                    .compareTo(createTime) > 0)
+                .collect(Collectors.toList());
+        Collections.sort(results, (a, b) -> new BuiltInFirst().compare(
+                a.get("name").toString(), b.get("name").toString()));
         return results;
     }
 
+    private Map<String, Object> graphSpaceView(
+            HugeClient client, Map<String, Object> source) {
+        Map<String, Object> info = new HashMap<>(source);
+        removeSensitiveFields(info);
+        String name = info.get("name").toString();
+        info.put("graphspace_admin",
+                 userService.listGraphSpaceAdmin(client, name));
+        info.put("statistic", evCount(client, name));
+        return info;
+    }
+
     public List<Map<String, Object>> queryAccessibleGs(HugeClient client,
                                                        String query,
                                                        String createTime) {
+        return queryAccessibleSpaces(client, query, createTime).stream()
+                .map(space -> accessibleView(client, space))
+                .collect(Collectors.toList());
+    }
+
+    public IPage<Map<String, Object>> queryAccessibleGsPage(
+            HugeClient client, String query, String createTime,
+            int pageNo, int pageSize) {
+        return pageAndMap(queryAccessibleSpaces(client, query, createTime),
+                          pageNo, pageSize,
+                          space -> accessibleView(client, space));
+    }
+
+    private List<GraphSpace> queryAccessibleSpaces(HugeClient client,
+                                                   String query,
+                                                   String createTime) {
         String prefix = query == null ? "" : query;
         String after = createTime == null ? "" : createTime;
-        List<Map<String, Object>> results = client.graphSpace()
+        List<GraphSpace> results = client.graphSpace()
                 .listGraphSpace().stream()
                 .map(client.graphSpace()::getGraphSpace)
                 .filter(space -> space != null &&
@@ -160,37 +189,93 @@ public class GraphSpaceService {
                 .filter(space -> !space.isAuth() ||
                                  client.auth().isSpaceAdmin(space.getName()) ||
                                  hasCurrentUserAccess(client, space.getName()))
-                .map(space -> {
-                    GraphSpaceEntity entity =
-                            GraphSpaceEntity.fromGraphSpace(space);
-                    entity.setStatistic(evCount(client, space.getName()));
-                    Map<String, Object> info = toView(entity);
-                    info.put("authed", true);
-                    info.put("default", false);
-                    return info;
-                })
                 .collect(Collectors.toList());
-        Collections.sort(results, (a, b) ->
-                new BuiltInFirst().compare(a.get("name").toString(),
-                                           b.get("name").toString()));
+        Collections.sort(results, (a, b) -> new BuiltInFirst().compare(
+                a.getName(), b.getName()));
         return results;
     }
 
+    private Map<String, Object> accessibleView(HugeClient client,
+                                               GraphSpace space) {
+        GraphSpaceEntity entity = GraphSpaceEntity.fromGraphSpace(space);
+        entity.setStatistic(evCount(client, space.getName()));
+        Map<String, Object> info = toView(entity);
+        info.put("authed", true);
+        info.put("default", false);
+        return info;
+    }
+
     private static boolean hasCurrentUserAccess(HugeClient client,
                                                 String graphSpace) {
+        if (!client.supportsDefaultRole()) {
+            return client.auth().isSpaceMember(graphSpace);
+        }
         if (client.auth().checkDefaultRole(graphSpace, "analyst")) {
             return true;
         }
-        return client.supportsDefaultRole() &&
-               client.auth().checkDefaultRole(graphSpace, "observer");
+        return client.auth().checkDefaultRole(graphSpace, "observer");
     }
 
     public List<Map<String, Object>> queryAnonymousGs(HugeClient client,
                                                       String query,
                                                       String createTime) {
+        return queryAnonymousSpaces(client, query, createTime).stream()
+                .map(space -> anonymousView(client, space))
+                .collect(Collectors.toList());
+    }
+
+    public List<String> listAnonymous(HugeClient client) {
+        return queryAnonymousSpaces(client, "", "").stream()
+                .map(GraphSpace::getName)
+                .collect(Collectors.toList());
+    }
+
+    public Map<String, Object> getAnonymous(HugeClient client,
+                                            String graphSpace) {
+        return anonymousView(client, publicSpace(client, graphSpace));
+    }
+
+    public boolean isAuthForAnonymous(HugeClient client, String graphSpace) {
+        return publicSpace(client, graphSpace).isAuth();
+    }
+
+    public IPage<Map<String, Object>> queryAnonymousGsPage(
+            HugeClient client, String query, String createTime,
+            int pageNo, int pageSize) {
+        List<GraphSpace> spaces =
+                queryAnonymousSpaces(client, query, createTime);
+        return pageAndMap(spaces, pageNo, pageSize,
+                          space -> anonymousView(client, space));
+    }
+
+    static <S, T> IPage<T> pageAndMap(
+            List<S> sourceRecords, int pageNo, int pageSize,
+            Function<S, T> mapper) {
+        int boundedSize = PageUtil.boundedSize(pageSize);
+        if (pageSize == -1 && sourceRecords.size() > boundedSize) {
+            throw new IllegalArgumentException(
+                      "GraphSpace list exceeds the maximum all-record limit of " +
+                      boundedSize);
+        }
+        IPage<S> source = PageUtil.page(sourceRecords, pageNo, pageSize);
+        List<T> records = source.getRecords().stream()
+                .map(mapper)
+                .collect(Collectors.toList());
+        Page<T> result =
+                new Page<>(source.getCurrent(), source.getSize(),
+                           sourceRecords.size(), true);
+        result.setRecords(records);
+        result.setOrders(Collections.emptyList());
+        result.setPages(source.getPages());
+        return result;
+    }
+
+    private List<GraphSpace> queryAnonymousSpaces(HugeClient client,
+                                                  String query,
+                                                  String createTime) {
         String prefix = query == null ? "" : query;
         String after = createTime == null ? "" : createTime;
-        List<Map<String, Object>> results = client.graphSpace()
+        List<GraphSpace> results = client.graphSpace()
                 .listGraphSpace().stream()
                 .map(client.graphSpace()::getGraphSpace)
                 .filter(space -> space != null &&
@@ -198,22 +283,46 @@ public class GraphSpaceService {
                                   space.getNickname() != null &&
                                   space.getNickname().contains(prefix)))
                 .filter(space -> space.getCreateTime() == null || space.getCreateTime().compareTo(after) > 0)
-                .map(space -> {
-                    GraphSpaceEntity entity =
-                            GraphSpaceEntity.fromGraphSpace(space);
-                    entity.setStatistic(evCount(client, space.getName()));
-                    Map<String, Object> info = toView(entity);
-                    info.put("authed", true);
-                    info.put("default", false);
-                    return info;
-                })
+                .filter(space -> !space.isAuth())
                 .collect(Collectors.toList());
-        Collections.sort(results, (a, b) ->
-                new BuiltInFirst().compare(a.get("name").toString(),
-                                           b.get("name").toString()));
+        Collections.sort(results, (a, b) -> new BuiltInFirst().compare(
+                a.getName(), b.getName()));
         return results;
     }
 
+    private Map<String, Object> anonymousView(HugeClient client,
+                                              GraphSpace space) {
+        GraphSpaceEntity entity = GraphSpaceEntity.fromGraphSpace(space);
+        entity.setStatistic(evCount(client, space.getName()));
+        Map<String, Object> info = toView(entity);
+        info.put("authed", true);
+        info.put("default", false);
+        return info;
+    }
+
+    private static GraphSpace publicSpace(HugeClient client,
+                                          String graphSpace) {
+        GraphSpace space;
+        try {
+            space = client.graphSpace().getGraphSpace(graphSpace);
+        } catch (ServerException e) {
+            if (e.status() == 400 || e.status() == 401 ||
+                e.status() == 403 || e.status() == 404) {
+                throw unavailableGraphSpace();
+            }
+            throw e;
+        }
+        if (space == null || space.isAuth()) {
+            throw unavailableGraphSpace();
+        }
+        return space;
+    }
+
+    private static ExternalException unavailableGraphSpace() {
+        return new ExternalException(HttpStatus.NOT_FOUND.value(),
+                                     "GraphSpace is unavailable");
+    }
+
     public Map<String, Object> toView(GraphSpaceEntity entity) {
         Map<String, Object> info = HubbleUtil.uncheckedCast(
                 JsonUtil.fromJson(JsonUtil.toJson(entity), Map.class));
diff --git a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties
index 8b9cfa5e7..60f221907 100644
--- a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties
+++ b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages.properties
@@ -33,8 +33,14 @@ auth.user.import-file.failed=Failed to prepare the uploaded user import file
 auth.user.import-csv.failed=Failed to parse the uploaded user CSV file
 auth.permission-preset.unsupported=This server cannot apply GraphSpace permission presets. Connect Hubble to a distributed Server that supports GraphSpace permission presets.
 auth.permission-preset.required=Custom GraphSpace role assignments are disabled on this Server. Use one of the permission presets shown in Hubble.
+auth.permission-preset.account-required=Choose an account permission preset.
+auth.permission-preset.superadmin-mismatch=The account permission preset does not match its global administrator flag.
+auth.permission-preset.graphspace-required=Choose at least one GraphSpace for this permission preset.
 auth.permission-preset.graphspace-not-found=GraphSpace ''{0}'' is unavailable. Refresh the GraphSpace list and select an existing one.
 auth.permission-preset.invalid=Permission preset ''{0}'' is unsupported. Choose a preset shown in the account form.
+auth.permission-preset.mismatch=GraphSpace permission preset ''{0}'' does not match account preset ''{1}''.
+auth.permission-preset.entry-invalid=GraphSpace permission entries must be objects.
+auth.profile.update-unsupported=This Server supports a read-only profile. Upgrade it to edit profile details.
 common.name-time-order.conflict=The param name_order and time_order cannot set at same time
 common.param.path-id-should-same-as-body=The id in path({0}) must be same as request body({1}) if it exists
 
diff --git a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties
index 7b92c919a..ba12d14c6 100644
--- a/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties
+++ b/hugegraph-hubble/hubble-be/src/main/resources/i18n/messages_zh_CN.properties
@@ -33,8 +33,14 @@ auth.user.import-file.failed=无法准备上传的用户导入文件
 auth.user.import-csv.failed=无法解析上传的用户 CSV 文件
 auth.permission-preset.unsupported=当前 Server 无法应用 GraphSpace 权限预设。请将 Hubble 连接到支持 GraphSpace 权限预设的分布式 Server。
 auth.permission-preset.required=当前 Server 已禁用 GraphSpace 自定义角色分配。请使用 Hubble 提供的权限预设。
+auth.permission-preset.account-required=请选择账号权限预设。
+auth.permission-preset.superadmin-mismatch=账号权限预设与全局管理员标记不一致。
+auth.permission-preset.graphspace-required=请为此权限预设至少选择一个 GraphSpace。
 auth.permission-preset.graphspace-not-found=GraphSpace“{0}”不可用。请刷新 GraphSpace 列表后选择一个现有空间。
 auth.permission-preset.invalid=不支持权限预设“{0}”。请选择账号表单中提供的预设。
+auth.permission-preset.mismatch=GraphSpace 权限预设“{0}”与账号权限预设“{1}”不一致。
+auth.permission-preset.entry-invalid=GraphSpace 权限条目必须是对象。
+auth.profile.update-unsupported=当前 Server 版本仅支持只读个人资料。请升级 Server 后再编辑资料。
 common.name-time-order.conflict=参数 name_order 和 time_order 不能同时设置
 common.param.path-id-should-same-as-body=当请求体中的 id({1}) 存在时,必须与路径中的 ({0}) 相同
 
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java
index 3eaefb867..85b6dbcd7 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java
@@ -504,6 +504,7 @@ public class GraphSpaceAuthOwnershipTest {
         ReflectionTestUtils.setField(service, "belongService", belongs);
 
         service.applySpacePreset(this.client, "SPACE_A", "graph-user",
+                                 "graph-user",
                                  "GS_READ_ONLY");
 
         Mockito.verify(this.auth).addSpaceMember("graph-user", "SPACE_A");
@@ -614,11 +615,12 @@ public class GraphSpaceAuthOwnershipTest {
         mvc.perform(put("/api/v1.3/graphspaces/SPACE/auth/users/" +
                         "new-member/preset")
                     .contentType(MediaType.APPLICATION_JSON)
-                    .content("{\"permission_preset\":\"GS_READ_WRITE\"}"))
+                    .content("{\"username\":\"new-member\"," +
+                             "\"permission_preset\":\"GS_READ_WRITE\"}"))
            .andExpect(status().isOk());
 
         Mockito.verify(members).applySpacePreset(
-                this.client, "SPACE", "new-member", "GS_READ_WRITE");
+                this.client, "SPACE", null, "new-member", "GS_READ_WRITE");
         Mockito.verify(this.auth, Mockito.never())
                .getUser(Mockito.anyString());
     }
@@ -645,7 +647,8 @@ public class GraphSpaceAuthOwnershipTest {
         mvc.perform(put("/api/v1.3/graphspaces/SPACE/auth/users/" +
                         "global-admin/preset")
                     .contentType(MediaType.APPLICATION_JSON)
-                    .content("{\"permission_preset\":\"GS_READ_WRITE\"}"))
+                    .content("{\"username\":\"global-admin\"," +
+                             "\"permission_preset\":\"GS_READ_WRITE\"}"))
            .andExpect(status().isForbidden());
 
         Mockito.verifyZeroInteractions(members);
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java
index cc256318b..7911be002 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java
@@ -39,6 +39,7 @@ import org.apache.hugegraph.exception.ForbiddenException;
 import org.apache.hugegraph.handler.ExceptionAdvisor;
 import org.apache.hugegraph.options.HubbleOptions;
 import org.apache.hugegraph.service.auth.UserService;
+import org.apache.hugegraph.service.auth.AuthModeService;
 import org.apache.hugegraph.service.graphs.GraphsService;
 import org.apache.hugegraph.service.space.GraphSpaceService;
 import org.apache.hugegraph.structure.space.GraphSpace;
@@ -107,6 +108,50 @@ public class GraphSpaceControllerTest {
         Assert.assertFalse(detail.containsKey("configs"));
     }
 
+    @Test
+    public void testAnonymousEndpointsUseOnlyPublicGraphSpaces() {
+        HugeClient client = Mockito.mock(HugeClient.class);
+        GraphSpaceService graphSpaceService = Mockito.mock(
+                                                GraphSpaceService.class);
+        TestGraphSpaceController controller =
+                new TestGraphSpaceController(client);
+        HugeConfig config = Mockito.mock(HugeConfig.class);
+        Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)).thenReturn(false);
+        AuthModeService authMode = new AuthModeService(config);
+        Mockito.when(graphSpaceService.listAnonymous(client))
+               .thenReturn(java.util.Collections.singletonList("public"));
+        Mockito.when(graphSpaceService.getAnonymous(client, "public"))
+               .thenReturn(java.util.Collections.singletonMap("name", "public"));
+        Mockito.when(graphSpaceService.isAuthForAnonymous(client, "public"))
+               .thenReturn(false);
+        controller.config = config;
+        ReflectionTestUtils.setField(controller, "authMode", authMode);
+        ReflectionTestUtils.setField(controller, "graphSpaceService",
+                                     graphSpaceService);
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> names = (Map<String, Object>) controller.list();
+        @SuppressWarnings("unchecked")
+        Map<String, Object> detail =
+                (Map<String, Object>) controller.get("public");
+        @SuppressWarnings("unchecked")
+        Map<String, Object> auth =
+                (Map<String, Object>) controller.isAuth("public");
+
+        Assert.assertEquals(java.util.Collections.singletonList("public"),
+                            names.get("graphspaces"));
+        Assert.assertEquals("public", detail.get("name"));
+        Assert.assertEquals(false, auth.get("auth"));
+        Mockito.verify(graphSpaceService, Mockito.never()).listAll(client);
+        Mockito.verify(graphSpaceService, Mockito.never())
+               .isAuth(client, "public");
+        Mockito.verify(graphSpaceService, Mockito.never())
+               .getWithoutAdmins(client, "public");
+        Mockito.verify(graphSpaceService, Mockito.never())
+               .evCount(client, "public");
+    }
+
     @Test
     public void testApplyDefaultsForOptionalResourceLimits() {
         GraphSpaceEntity graphSpace = new GraphSpaceEntity();
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java
index c0151aca4..aa31149ad 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java
@@ -18,6 +18,8 @@
 
 package org.apache.hugegraph.controller.space;
 
+import java.util.Collections;
+
 import org.junit.Test;
 import org.mockito.Mockito;
 
@@ -25,6 +27,9 @@ import org.apache.hugegraph.config.HugeConfig;
 import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.exception.ForbiddenException;
 import org.apache.hugegraph.options.HubbleOptions;
+import org.apache.hugegraph.service.auth.AuthModeService;
+import org.apache.hugegraph.service.auth.UserService;
+import org.apache.hugegraph.service.space.SchemaTemplateService;
 import org.apache.hugegraph.structure.space.SchemaTemplate;
 import org.apache.hugegraph.testutil.Assert;
 
@@ -32,7 +37,8 @@ public class SchemaTemplateControllerSecurityTest {
 
     @Test
     public void testCreateRequiresGraphSpaceWrite() {
-        TestController controller = controller();
+        TestController controller = controller(true);
+        controller.denyWrite();
 
         Assert.assertThrows(ForbiddenException.class, () ->
                 controller.create("space", Mockito.mock(SchemaTemplate.class)));
@@ -40,7 +46,8 @@ public class SchemaTemplateControllerSecurityTest {
 
     @Test
     public void testUpdateRequiresGraphSpaceWrite() {
-        TestController controller = controller();
+        TestController controller = controller(true);
+        controller.denyWrite();
 
         Assert.assertThrows(ForbiddenException.class, () ->
                 controller.update("space", "template",
@@ -49,26 +56,131 @@ public class SchemaTemplateControllerSecurityTest {
 
     @Test
     public void testDeleteRequiresGraphSpaceWrite() {
-        TestController controller = controller();
+        TestController controller = controller(true);
+        controller.denyWrite();
+
+        Assert.assertThrows(ForbiddenException.class, () ->
+                controller.delete("space", "template"));
+    }
+
+    @Test
+    public void testOwnerCanUpdateAndDeleteTemplate() {
+        TestController controller = controller(true);
+        SchemaTemplate template = new SchemaTemplate("template", "schema");
+        Mockito.when(controller.schemaTemplateService.get(
+                     controller.client, "template"))
+               .thenReturn(Collections.singletonMap("creator", "alice"));
+
+        controller.update("space", "template", template);
+        controller.delete("space", "template");
+
+        Mockito.verify(controller.schemaTemplateService)
+               .update(controller.client, template);
+        Mockito.verify(controller.schemaTemplateService)
+               .delete(controller.client, "template");
+    }
 
+    @Test
+    public void testWriterCannotMutateAnotherUsersTemplate() {
+        TestController controller = controller(true);
+        Mockito.when(controller.schemaTemplateService.get(
+                     controller.client, "template"))
+               .thenReturn(Collections.singletonMap("creator", "bob"));
+
+        Assert.assertThrows(ForbiddenException.class, () ->
+                controller.update("space", "template",
+                                  Mockito.mock(SchemaTemplate.class)));
         Assert.assertThrows(ForbiddenException.class, () ->
                 controller.delete("space", "template"));
+        Mockito.verify(controller.schemaTemplateService, Mockito.never())
+               .update(Mockito.any(), Mockito.any());
+        Mockito.verify(controller.schemaTemplateService, Mockito.never())
+               .delete(Mockito.any(), Mockito.anyString());
+    }
+
+    @Test
+    public void testGraphSpaceManagerCanMutateAnyTemplate() {
+        TestController controller = controller(true);
+        Mockito.when(controller.users.isAssignSpaceAdmin(
+                     controller.client, "space"))
+               .thenReturn(true);
+
+        controller.delete("space", "template");
+
+        Mockito.verify(controller.schemaTemplateService)
+               .delete(controller.client, "template");
+        Mockito.verify(controller.schemaTemplateService, Mockito.never())
+               .get(Mockito.any(), Mockito.anyString());
     }
 
-    private static TestController controller() {
+    @Test
+    public void testGlobalAdministratorCanMutateAnyTemplate() {
+        TestController controller = controller(true);
+        Mockito.when(controller.users.isSuperAdmin(controller.client))
+               .thenReturn(true);
+
+        controller.delete("space", "template");
+
+        Mockito.verify(controller.schemaTemplateService)
+               .delete(controller.client, "template");
+        Mockito.verify(controller.schemaTemplateService, Mockito.never())
+               .get(Mockito.any(), Mockito.anyString());
+    }
+
+    @Test
+    public void testAnonymousModeKeepsTemplateMutationAvailable() {
+        TestController controller = controller(false);
+
+        controller.delete("space", "template");
+
+        Mockito.verify(controller.schemaTemplateService)
+               .delete(controller.client, "template");
+        Mockito.verifyZeroInteractions(controller.users);
+    }
+
+    private static TestController controller(boolean authEnabled) {
         TestController controller = new TestController();
         HugeConfig config = Mockito.mock(HugeConfig.class);
         Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(config.get(HubbleOptions.AUTH_ENABLED))
+               .thenReturn(authEnabled);
         controller.config = config;
+        controller.setAuthMode(new AuthModeService(config));
+        controller.schemaTemplateService =
+                Mockito.mock(SchemaTemplateService.class);
         return controller;
     }
 
     private static class TestController extends SchemaTemplateController {
 
+        private final HugeClient client = Mockito.mock(HugeClient.class);
+        private final UserService users = Mockito.mock(UserService.class);
+        private boolean writeAllowed = true;
+
+        private TestController() {
+            this.userService = this.users;
+        }
+
+        public void setAuthMode(AuthModeService authMode) {
+            this.authMode = authMode;
+        }
+
+        public void denyWrite() {
+            this.writeAllowed = false;
+        }
+
         @Override
         protected HugeClient requireGraphSpaceWrite(String graphSpace) {
-            throw new ForbiddenException(
-                    "Permission denied: write graphspace resources");
+            if (!this.writeAllowed) {
+                throw new ForbiddenException(
+                        "Permission denied: write graphspace resources");
+            }
+            return this.client;
+        }
+
+        @Override
+        protected String getUser() {
+            return "alice";
         }
     }
 }
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
index 7f1b082b3..1091906ff 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
@@ -108,6 +108,8 @@ public class AuthContextServiceTest {
     public void testPdServerWithoutPresetApiHidesScopedMutations() {
         Fixture fixture = new Fixture(true);
         Mockito.when(fixture.client.supportsDefaultRole()).thenReturn(false);
+        Mockito.when(fixture.client.supportsPersonalProfileUpdate())
+               .thenReturn(false);
         Mockito.when(fixture.users.getpersonal(fixture.client, "alice"))
                .thenReturn(user(true, Collections.singletonList("space-a")));
 
@@ -121,6 +123,8 @@ public class AuthContextServiceTest {
         Assert.assertTrue(actions(context, "members").isEmpty());
         Assert.assertTrue(actions(context, "roles").isEmpty());
         Assert.assertTrue(actions(context, "authorizations").isEmpty());
+        Assert.assertEquals(Set.of("read", "change_password"),
+                            actions(context, "account"));
     }
 
     @Test
@@ -342,6 +346,8 @@ public class AuthContextServiceTest {
         private Fixture(boolean pdEnabled) {
             Mockito.when(this.config.get(HubbleOptions.PD_ENABLED))
                    .thenReturn(pdEnabled);
+            Mockito.when(this.client.supportsPersonalProfileUpdate())
+                   .thenReturn(true);
             this.service = new AuthContextService(this.config, this.users);
         }
     }
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
index a9489d97c..080150e0a 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.hugegraph.service.auth;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -78,6 +79,80 @@ public class GraphSpaceUserServiceTest {
                             error.getMessage());
     }
 
+    @Test
+    public void testGraphSpacePresetRequiresAtLeastOneGraphSpace() {
+        for (String preset : Arrays.asList(
+                "GS_READ_ONLY", "GS_READ_WRITE", "GS_ADMIN")) {
+            ParameterizedException error = null;
+            try {
+                this.service.validatePermissionPresets(
+                        this.client, Collections.emptyList(), preset);
+            } catch (ParameterizedException e) {
+                error = e;
+            }
+
+            Assert.assertNotNull(error);
+            Assert.assertEquals(
+                    "auth.permission-preset.graphspace-required",
+                    error.getMessage());
+        }
+        Mockito.verifyZeroInteractions(this.graphSpace);
+    }
+
+    @Test
+    public void testUnknownAccountPresetIsRejectedBeforeAccountCreation() {
+        ParameterizedException error = null;
+        try {
+            this.service.validatePermissionPresets(
+                    this.client, Collections.emptyList(), "UNKNOWN");
+        } catch (ParameterizedException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertEquals("auth.permission-preset.invalid",
+                            error.getMessage());
+        Mockito.verifyZeroInteractions(this.graphSpace);
+    }
+
+    @Test
+    public void testMixedGraphSpacePresetsAreRejected() {
+        Mockito.when(this.graphSpace.listGraphSpace())
+               .thenReturn(Collections.singletonList("team"));
+        Map<String, String> permission = new HashMap<>();
+        permission.put("graphspace", "team");
+        permission.put("permission_preset", "GS_READ_ONLY");
+
+        ParameterizedException error = null;
+        try {
+            this.service.validatePermissionPresets(
+                    this.client, Collections.singletonList(permission),
+                    "GS_READ_WRITE");
+        } catch (ParameterizedException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertEquals("auth.permission-preset.mismatch",
+                            error.getMessage());
+    }
+
+    @Test
+    public void testNullGraphSpacePermissionEntryIsRejected() {
+        ParameterizedException error = null;
+        try {
+            this.service.validatePermissionPresets(
+                    this.client, Collections.singletonList(null),
+                    "GS_READ_ONLY");
+        } catch (ParameterizedException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertEquals("auth.permission-preset.entry-invalid",
+                            error.getMessage());
+    }
+
     @Test
     public void testListUsersMarksOnlyServerDefaultRolesAsPresets() {
         User user = user("u-1", "alice");
@@ -185,6 +260,102 @@ public class GraphSpaceUserServiceTest {
         Mockito.verify(this.auth).delSpaceMember("alice", "old");
     }
 
+    @Test
+    public void testNewAccountAppliesOnlyRequestedSpaces() {
+        User user = user("u-1", "alice");
+        Mockito.when(this.client.findUserByName("alice")).thenReturn(user);
+        GraphSpaceUserService service = Mockito.spy(this.service);
+        Mockito.doNothing().when(service)
+               .applySpacePreset(this.client, "team", "u-1",
+                                 "alice",
+                                 "GS_READ_ONLY");
+
+        service.applyPermissionPresetsForNewAccount(
+                this.client, "alice",
+                Collections.singletonList(permission("team", "GS_READ_ONLY")),
+                "GS_READ_ONLY");
+
+        Mockito.verify(service).applySpacePreset(
+                this.client, "team", "u-1", "alice", "GS_READ_ONLY");
+        Mockito.verify(this.graphSpace, Mockito.never()).listGraphSpace();
+    }
+
+    @Test
+    public void testReconciliationRestoresEarlierGraphSpaces() {
+        User user = user("u-1", "alice");
+        Mockito.when(this.client.findUserByName("alice")).thenReturn(user);
+        Mockito.when(this.graphSpace.listGraphSpace())
+               .thenReturn(Arrays.asList("first", "second"));
+        Mockito.when(this.auth.listSpaceMember("first"))
+               .thenReturn(Collections.singletonList("alice"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSpaceMember("second"))
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.belongService.list(
+                             Mockito.eq(this.client), Mockito.anyString(),
+                             Mockito.isNull(), Mockito.eq("u-1")))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "first", "alice", "analyst"))
+               .thenReturn(false);
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "first", "alice", "observer"))
+               .thenReturn(true, false);
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "second", "alice", "analyst"))
+               .thenReturn(false);
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "second", "alice", "observer"))
+               .thenReturn(false);
+        Mockito.when(this.auth.listSpaceAdmin(Mockito.anyString()))
+               .thenReturn(Collections.emptyList());
+        GraphSpaceUserService service = Mockito.spy(this.service);
+        Mockito.doNothing().when(service)
+               .applySpacePreset(this.client, "first", "u-1",
+                                 "alice",
+                                 "GS_READ_ONLY");
+        RuntimeException failure = new RuntimeException("second failed");
+        Mockito.doThrow(failure).when(service)
+               .applySpacePreset(this.client, "second", "u-1",
+                                 "alice",
+                                 "GS_READ_ONLY");
+
+        Throwable error = Assert.assertThrows(
+                RuntimeException.class,
+                () -> service.applyPermissionPresets(
+                        this.client, "alice",
+                        Arrays.asList(
+                                permission("first", "GS_READ_ONLY"),
+                                permission("second", "GS_READ_ONLY")),
+                        "GS_READ_ONLY"));
+
+        Assert.assertSame(failure, error);
+        InOrder rollback = Mockito.inOrder(this.auth, this.graphSpace);
+        rollback.verify(this.auth).addSpaceMember("alice", "first");
+        rollback.verify(this.graphSpace)
+                .setDefaultRole("first", "alice", "observer");
+    }
+
+    @Test
+    public void testNewMemberResolvesIdAfterUsernameMembership() {
+        User user = user("u-1", "alice");
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.client.findUserByName("alice")).thenReturn(user);
+        Mockito.when(this.belongService.list(
+                             this.client, "team", null, "u-1"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSpaceAdmin("team"))
+               .thenReturn(Collections.emptyList());
+
+        this.service.applySpacePreset(this.client, "team", null, "alice", "GS_READ_ONLY");
+
+        Mockito.verify(this.auth).addSpaceMember("alice", "team");
+        Mockito.verify(this.client).findUserByName("alice");
+        Mockito.verify(this.graphSpace)
+               .setDefaultRole("team", "alice", "observer");
+    }
+
     @Test
     public void testApplyAdminPresetAddsManagementAndWriteAccess() {
         User user = user("alice", "alice");
@@ -198,6 +369,7 @@ public class GraphSpaceUserServiceTest {
                .thenReturn(Collections.emptyList());
 
         this.service.applySpacePreset(this.client, "team", "alice",
+                                      "alice",
                                       "GS_ADMIN");
 
         Mockito.verify(this.auth).addSpaceMember("alice", "team");
@@ -219,6 +391,7 @@ public class GraphSpaceUserServiceTest {
                .thenReturn(Collections.singletonList("alice"));
 
         this.service.applySpacePreset(this.client, "team", "alice",
+                                      "alice",
                                       "GS_READ_WRITE");
 
         Mockito.verify(this.auth).delSpaceAdmin("alice", "team");
@@ -241,6 +414,7 @@ public class GraphSpaceUserServiceTest {
                .thenReturn(Collections.emptyList());
 
         this.service.applySpacePreset(this.client, "team", "alice",
+                                      "alice",
                                       "GS_READ_ONLY");
 
         InOrder order = Mockito.inOrder(this.auth);
@@ -261,6 +435,7 @@ public class GraphSpaceUserServiceTest {
         Assert.assertThrows(RuntimeException.class,
                             () -> this.service.applySpacePreset(
                                     this.client, "team", "alice",
+                                    "alice",
                                     "GS_READ_ONLY"));
 
         InOrder order = Mockito.inOrder(this.auth);
@@ -280,6 +455,7 @@ public class GraphSpaceUserServiceTest {
         Assert.assertThrows(RuntimeException.class,
                             () -> this.service.applySpacePreset(
                                     this.client, "team", "alice",
+                                    "alice",
                                     "GS_READ_ONLY"));
 
         Mockito.verify(this.auth).delSpaceMember("alice", "team");
@@ -305,6 +481,7 @@ public class GraphSpaceUserServiceTest {
         Assert.assertThrows(RuntimeException.class,
                             () -> this.service.applySpacePreset(
                                     this.client, "team", "alice",
+                                    "alice",
                                     "GS_READ_ONLY"));
 
         InOrder order = Mockito.inOrder(this.auth, this.graphSpace);
@@ -335,6 +512,7 @@ public class GraphSpaceUserServiceTest {
         Assert.assertThrows(RuntimeException.class,
                             () -> this.service.applySpacePreset(
                                     this.client, "team", "alice",
+                                    "alice",
                                     "GS_ADMIN"));
 
         InOrder order = Mockito.inOrder(this.auth, this.graphSpace);
@@ -379,6 +557,7 @@ public class GraphSpaceUserServiceTest {
         Assert.assertThrows(RuntimeException.class,
                             () -> this.service.applySpacePreset(
                                     this.client, "team", "alice",
+                                    "alice",
                                     "GS_READ_WRITE"));
 
         Mockito.verify(this.belongService)
@@ -429,6 +608,7 @@ public class GraphSpaceUserServiceTest {
         Assert.assertThrows(RuntimeException.class,
                             () -> this.service.applySpacePreset(
                                     this.client, "team", "alice",
+                                    "alice",
                                     "GS_READ_ONLY"));
 
         Mockito.verify(this.belongService)
@@ -462,6 +642,7 @@ public class GraphSpaceUserServiceTest {
         Assert.assertThrows(RuntimeException.class,
                             () -> this.service.applySpacePreset(
                                     this.client, "team", "alice",
+                                    "alice",
                                     "GS_READ_ONLY"));
 
         Mockito.verify(this.graphSpace)
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
index 4a05ea346..6c051e7ba 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
@@ -21,14 +21,21 @@ import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.apache.hugegraph.driver.AuthManager;
 import org.apache.hugegraph.driver.GraphSpaceManager;
 import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.entity.space.GraphSpaceEntity;
+import org.apache.hugegraph.exception.ExternalException;
+import org.apache.hugegraph.exception.ServerException;
 import org.apache.hugegraph.service.auth.UserService;
 import org.apache.hugegraph.service.graphs.GraphsService;
 import org.apache.hugegraph.structure.space.GraphSpace;
+import org.apache.hugegraph.util.PageUtil;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
@@ -151,7 +158,7 @@ public class GraphSpaceServiceTest {
     }
 
     @Test
-    public void testLegacyAnalystGraphSpaceRemainsVisible() {
+    public void testLegacyMemberGraphSpaceRemainsVisible() {
         GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
         AuthManager auth = Mockito.mock(AuthManager.class);
         GraphSpace space = graphSpace("legacy", true, "20260712");
@@ -161,8 +168,7 @@ public class GraphSpaceServiceTest {
         Mockito.when(manager.listGraphSpace())
                .thenReturn(java.util.Collections.singletonList("legacy"));
         Mockito.when(manager.getGraphSpace("legacy")).thenReturn(space);
-        Mockito.when(auth.checkDefaultRole("legacy", "analyst"))
-               .thenReturn(true);
+        Mockito.when(auth.isSpaceMember("legacy")).thenReturn(true);
         Mockito.when(this.graphsService.listGraphNames(this.client, "legacy",
                                                        ""))
                .thenReturn(java.util.Collections.emptySet());
@@ -173,7 +179,7 @@ public class GraphSpaceServiceTest {
         Assert.assertEquals(1, response.size());
         Assert.assertEquals("legacy", response.get(0).get("name"));
         Mockito.verify(auth, Mockito.never())
-               .checkDefaultRole("legacy", "observer");
+               .checkDefaultRole(Mockito.anyString(), Mockito.anyString());
     }
 
     @Test
@@ -201,6 +207,154 @@ public class GraphSpaceServiceTest {
         Assert.assertTrue((Boolean) response.get(0).get("authed"));
     }
 
+    @Test
+    public void testAnonymousGraphSpacesExcludeProtectedSpaces() {
+        GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
+        GraphSpace visible = graphSpace("public", false, "20260712");
+        GraphSpace protectedSpace = graphSpace("protected", true, "20260712");
+        Mockito.when(this.client.graphSpace()).thenReturn(manager);
+        Mockito.when(manager.listGraphSpace())
+               .thenReturn(java.util.Arrays.asList("public", "protected"));
+        Mockito.when(manager.getGraphSpace("public")).thenReturn(visible);
+        Mockito.when(manager.getGraphSpace("protected"))
+               .thenReturn(protectedSpace);
+        Mockito.when(this.graphsService.listGraphNames(this.client, "public", ""))
+               .thenReturn(java.util.Collections.emptySet());
+
+        List<Map<String, Object>> response =
+                this.service.queryAnonymousGs(this.client, "", "");
+
+        Assert.assertEquals(1, response.size());
+        Assert.assertEquals("public", response.get(0).get("name"));
+        Assert.assertEquals(java.util.Collections.singletonList("public"),
+                            this.service.listAnonymous(this.client));
+        Mockito.verify(this.graphsService, Mockito.never())
+               .listGraphNames(this.client, "protected", "");
+    }
+
+    @Test
+    public void testAnonymousDetailRejectsProtectedSpaceBeforeStatistics() {
+        GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
+        GraphSpace protectedSpace = graphSpace("protected", true, "20260712");
+        Mockito.when(this.client.graphSpace()).thenReturn(manager);
+        Mockito.when(manager.getGraphSpace("protected"))
+               .thenReturn(protectedSpace);
+
+        try {
+            this.service.getAnonymous(this.client, "protected");
+            Assert.fail("Expected protected GraphSpace to be unavailable");
+        } catch (ExternalException e) {
+            Assert.assertEquals(404, e.status());
+        }
+        try {
+            this.service.isAuthForAnonymous(this.client, "protected");
+            Assert.fail("Expected protected GraphSpace auth to be unavailable");
+        } catch (ExternalException e) {
+            Assert.assertEquals(404, e.status());
+        }
+        Mockito.verifyZeroInteractions(this.graphsService);
+    }
+
+    @Test
+    public void testAnonymousAuthHidesMissingGraphSpaceLikeProtectedSpace() {
+        GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
+        Mockito.when(this.client.graphSpace()).thenReturn(manager);
+        ServerException missing = new ServerException("missing");
+        missing.status(400);
+        Mockito.when(manager.getGraphSpace("missing")).thenThrow(missing);
+
+        try {
+            this.service.isAuthForAnonymous(this.client, "missing");
+            Assert.fail("Expected missing GraphSpace auth to be unavailable");
+        } catch (ExternalException e) {
+            Assert.assertEquals(404, e.status());
+        }
+        try {
+            this.service.getAnonymous(this.client, "missing");
+            Assert.fail("Expected missing GraphSpace detail to be unavailable");
+        } catch (ExternalException e) {
+            Assert.assertEquals(404, e.status());
+        }
+    }
+
+    @Test
+    public void testAnonymousGraphSpacesCollectStatisticsAfterPaging() {
+        GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
+        Mockito.when(this.client.graphSpace()).thenReturn(manager);
+        Mockito.when(manager.listGraphSpace())
+               .thenReturn(java.util.Arrays.asList("a", "b", "c"));
+        Mockito.when(manager.getGraphSpace(Mockito.anyString()))
+               .thenAnswer(invocation -> graphSpace(
+                       invocation.getArgument(0), false, "20260712"));
+        Mockito.when(this.graphsService.listGraphNames(
+                             Mockito.eq(this.client), Mockito.anyString(),
+                             Mockito.eq("")))
+               .thenReturn(java.util.Collections.emptySet());
+
+        IPage<Map<String, Object>> response =
+                this.service.queryAnonymousGsPage(this.client, "", "",
+                                                  2, 1);
+
+        Assert.assertEquals(3L, response.getTotal());
+        Assert.assertEquals(1, response.getRecords().size());
+        Assert.assertEquals("b", response.getRecords().get(0).get("name"));
+        Mockito.verify(this.graphsService)
+               .listGraphNames(this.client, "b", "");
+        Mockito.verify(this.graphsService, Mockito.never())
+               .listGraphNames(this.client, "a", "");
+        Mockito.verify(this.graphsService, Mockito.never())
+               .listGraphNames(this.client, "c", "");
+    }
+
+    @Test
+    public void testAccessibleGraphSpacesCollectStatisticsAfterPaging() {
+        GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
+        AuthManager auth = Mockito.mock(AuthManager.class);
+        Mockito.when(this.client.graphSpace()).thenReturn(manager);
+        Mockito.when(this.client.auth()).thenReturn(auth);
+        Mockito.when(manager.listGraphSpace())
+               .thenReturn(java.util.Arrays.asList("a", "b", "c"));
+        Mockito.when(manager.getGraphSpace(Mockito.anyString()))
+               .thenAnswer(invocation -> graphSpace(
+                       invocation.getArgument(0), false, "20260712"));
+        Mockito.when(this.graphsService.listGraphNames(
+                             Mockito.eq(this.client), Mockito.anyString(),
+                             Mockito.eq("")))
+               .thenReturn(java.util.Collections.emptySet());
+
+        IPage<Map<String, Object>> response =
+                this.service.queryAccessibleGsPage(this.client, "", "",
+                                                   2, 1);
+
+        Assert.assertEquals(3L, response.getTotal());
+        Assert.assertEquals("b", response.getRecords().get(0).get("name"));
+        Mockito.verify(this.graphsService)
+               .listGraphNames(this.client, "b", "");
+        Mockito.verify(this.graphsService, Mockito.never())
+               .listGraphNames(this.client, "a", "");
+        Mockito.verify(this.graphsService, Mockito.never())
+               .listGraphNames(this.client, "c", "");
+    }
+
+    @Test
+    public void testGraphSpaceAllSentinelUsesHardCap() {
+        List<Integer> values = IntStream.rangeClosed(0, PageUtil.HARD_CAP)
+                                        .boxed()
+                                        .collect(Collectors.toList());
+        AtomicInteger mapped = new AtomicInteger();
+
+        try {
+            GraphSpaceService.pageAndMap(values, 1, -1, value -> {
+                mapped.incrementAndGet();
+                return value;
+            });
+            Assert.fail("Expected the all-record hard cap to reject the request");
+        } catch (IllegalArgumentException e) {
+            Assert.assertTrue(e.getMessage().contains("10000"));
+        }
+        Assert.assertEquals(0, mapped.get());
+    }
+
     @Test
     public void testStatisticUsesActualFallbackDate() {
         Mockito.when(this.graphsService.listGraphNames(this.client, "space", ""))
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
index e947eb2a2..1f0568c98 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
@@ -203,6 +203,22 @@ public class AuthSecurityTest {
 
         MockHttpServletRequest context = new MockHttpServletRequest("GET", "/api/v1.3/auth/context");
         Assert.assertTrue(interceptor.preHandle(context, new MockHttpServletResponse(), null));
+
+        MockHttpServletRequest scopedUsers = new MockHttpServletRequest(
+                "GET", "/api/v1.3/graphspaces/DEFAULT/auth/users");
+        assertThrows(ExternalException.class, () ->
+                interceptor.preHandle(scopedUsers,
+                                      new MockHttpServletResponse(), null));
+
+        MockHttpServletRequest authGraph = new MockHttpServletRequest(
+                "GET", "/api/v1.3/graphspaces/DEFAULT/graphs/auth/schema");
+        Assert.assertTrue(interceptor.preHandle(
+                authGraph, new MockHttpServletResponse(), null));
+
+        MockHttpServletRequest authGraphSpace = new MockHttpServletRequest(
+                "GET", "/api/v1.3/graphspaces/auth/graphs/hugegraph/schema");
+        Assert.assertTrue(interceptor.preHandle(
+                authGraphSpace, new MockHttpServletResponse(), null));
     }
 
     @Test
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
index 24a9947d3..a3a796181 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
@@ -19,6 +19,8 @@ package org.apache.hugegraph.unit;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.junit.Assert;
@@ -28,13 +30,16 @@ import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 import org.springframework.test.util.ReflectionTestUtils;
 
+import org.apache.hugegraph.common.Response;
 import org.apache.hugegraph.config.HugeConfig;
 import org.apache.hugegraph.driver.AuthManager;
 import org.apache.hugegraph.driver.GraphSpaceManager;
 import org.apache.hugegraph.driver.GraphsManager;
 import org.apache.hugegraph.driver.HugeClient;
 import org.apache.hugegraph.entity.auth.UserEntity;
+import org.apache.hugegraph.exception.ParameterizedException;
 import org.apache.hugegraph.exception.ServerException;
+import org.apache.hugegraph.exception.UnauthorizedException;
 import org.apache.hugegraph.options.HubbleOptions;
 import org.apache.hugegraph.service.auth.GraphSpaceUserService;
 import org.apache.hugegraph.service.auth.UserService;
@@ -126,9 +131,9 @@ public class UserServiceCompatibilityTest {
     @Test
     public void testStandalonePersonalUpdateOmitsPdOnlyNickname() {
         Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(false);
-        Mockito.when(this.auth.getUserByName("user"))
-               .thenReturn(user("user"));
-        Mockito.when(this.client.findUserByName("user")).thenReturn(user("user"));
+        Mockito.when(this.client.supportsPersonalProfileUpdate())
+               .thenReturn(true);
+        Mockito.when(this.client.findCurrentUser("user")).thenReturn(user("user"));
 
         this.service.updatePersonal(this.client, "user", "display-name",
                                     "description");
@@ -139,11 +144,77 @@ public class UserServiceCompatibilityTest {
         Assert.assertEquals("description", request.getValue().description());
     }
 
+    @Test
+    public void testLegacyPersonalUpdateFailsBeforeServerWrite() {
+        Mockito.when(this.client.supportsPersonalProfileUpdate())
+               .thenReturn(false);
+
+        ParameterizedException error = null;
+        try {
+            this.service.updatePersonal(this.client, "user", "display-name",
+                                        "description");
+        } catch (ParameterizedException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertEquals("auth.profile.update-unsupported",
+                            error.getMessage());
+        Mockito.verify(this.client, Mockito.never())
+               .findCurrentUser(Mockito.anyString());
+        Mockito.verify(this.auth, Mockito.never())
+               .updateUser(Mockito.any(User.class));
+    }
+
+    @Test
+    public void testLegacyPasswordUpdateUsesVerifiedIdentity() {
+        User current = user("user");
+        current.setId("user-id");
+        Mockito.when(this.client.findCurrentUser("user"))
+               .thenReturn(current);
+
+        Response response = this.service.updatepwd(
+                this.client, "user", "old-password", "new-password");
+
+        Assert.assertEquals(200, response.getStatus());
+        ArgumentCaptor<User> request = ArgumentCaptor.forClass(User.class);
+        Mockito.verify(this.auth).updateUser(request.capture());
+        Assert.assertEquals("user-id", request.getValue().id());
+        Assert.assertEquals("new-password", request.getValue().password());
+    }
+
+    @Test
+    public void testCurrentUserIdentityMismatchIsUnauthorized() {
+        Mockito.when(this.client.findCurrentUser("user"))
+               .thenThrow(new IllegalStateException("mismatch"));
+
+        try {
+            this.service.getpersonal(this.client, "user");
+            Assert.fail("Expected an unauthorized current-user identity");
+        } catch (UnauthorizedException ignored) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testMissingCurrentUserRecordIsUnauthorized() {
+        ServerException missing = new ServerException("missing");
+        missing.status(404);
+        Mockito.when(this.client.findCurrentUser("user")).thenThrow(missing);
+
+        try {
+            this.service.getpersonal(this.client, "user");
+            Assert.fail("Expected an unauthorized missing current user");
+        } catch (UnauthorizedException ignored) {
+            // Expected
+        }
+    }
+
     @Test
     public void testCurrentUserPresetUsesSelfPermissionApi() {
         Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
         Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
-        Mockito.when(this.client.findUserByName("user"))
+        Mockito.when(this.client.findCurrentUser("user"))
                .thenReturn(user("user"));
         Mockito.when(this.graphSpace.listGraphSpace())
                .thenReturn(java.util.Collections.singletonList("SPACE"));
@@ -184,7 +255,7 @@ public class UserServiceCompatibilityTest {
     public void testCurrentUserIgnoresOnlyForbiddenGraphSpaces() {
         Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
         Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
-        Mockito.when(this.client.findUserByName("user"))
+        Mockito.when(this.client.findCurrentUser("user"))
                .thenReturn(user("user"));
         Mockito.when(this.graphSpace.listGraphSpace())
                .thenReturn(Arrays.asList("OWNED", "DENIED"));
@@ -202,15 +273,14 @@ public class UserServiceCompatibilityTest {
     }
 
     @Test
-    public void testLegacyAnalystKeepsGraphSpaceAccess() {
+    public void testLegacySpaceMemberKeepsGraphSpaceAccess() {
         Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
         Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
-        Mockito.when(this.client.findUserByName("user"))
+        Mockito.when(this.client.findCurrentUser("user"))
                .thenReturn(user("user"));
         Mockito.when(this.graphSpace.listGraphSpace())
                .thenReturn(Collections.singletonList("SPACE"));
-        Mockito.when(this.auth.checkDefaultRole("SPACE", "analyst"))
-               .thenReturn(true);
+        Mockito.when(this.auth.isSpaceMember("SPACE")).thenReturn(true);
 
         UserEntity result = this.service.getpersonal(this.client, "user");
 
@@ -218,7 +288,7 @@ public class UserServiceCompatibilityTest {
                             result.getResSpaces());
         Assert.assertEquals("LEGACY_CUSTOM", result.getPermissionPreset());
         Mockito.verify(this.auth, Mockito.never())
-               .checkDefaultRole("SPACE", "observer");
+               .checkDefaultRole(Mockito.anyString(), Mockito.anyString());
     }
 
     @Test
@@ -246,10 +316,6 @@ public class UserServiceCompatibilityTest {
     public void testModernProfileUpdatePreservesPermissionAssignments() {
         Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
         Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
-        GraphSpaceUserService actualGraphSpaceUsers =
-                new GraphSpaceUserService();
-        ReflectionTestUtils.setField(this.service, "graphSpaceUserService",
-                                     actualGraphSpaceUsers);
         UserEntity user = UserEntity.builder()
                                     .id("user-id")
                                     .name("user")
@@ -259,43 +325,89 @@ public class UserServiceCompatibilityTest {
         this.service.update(this.client, user);
 
         Mockito.verify(this.auth).updateUser(Mockito.any(User.class));
-        Mockito.verify(this.graphSpace, Mockito.never()).setDefaultRole(
-                Mockito.anyString(), Mockito.anyString(), Mockito.anyString());
-        Mockito.verify(this.graphSpace, Mockito.never()).deleteDefaultRole(
-                Mockito.anyString(), Mockito.anyString(), Mockito.anyString());
-        Mockito.verify(this.auth, Mockito.never()).addSpaceAdmin(
-                Mockito.anyString(), Mockito.anyString());
-        Mockito.verify(this.auth, Mockito.never()).delSpaceAdmin(
-                Mockito.anyString(), Mockito.anyString());
+        Mockito.verify(this.graphSpaceUsers, Mockito.never())
+               .validatePermissionPresets(Mockito.any(), Mockito.any(),
+                                          Mockito.any());
+        Mockito.verify(this.graphSpaceUsers, Mockito.never())
+               .applyPermissionPresets(Mockito.any(), Mockito.anyString(),
+                                       Mockito.any(), Mockito.any());
+        Mockito.verify(this.auth, Mockito.never()).listSuperAdmin();
+        Mockito.verify(this.auth, Mockito.never())
+               .addSuperAdmin(Mockito.anyString());
+        Mockito.verify(this.auth, Mockito.never())
+               .delSuperAdmin(Mockito.anyString());
     }
 
     @Test
-    public void testModernUserUpdateReconcilesAdminSpaces() {
+    public void testModernUserUpdateUsesPresetAsPermissionSource() {
         Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
         Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
-        Mockito.when(this.graphSpace.listGraphSpace())
-               .thenReturn(Arrays.asList("OLD", "NEW"));
-        Mockito.when(this.auth.listSpaceAdmin("OLD"))
-               .thenReturn(Collections.singletonList("user"));
-        Mockito.when(this.auth.listSpaceAdmin("NEW"))
-               .thenReturn(Collections.emptyList());
-        Mockito.when(this.client.findUserByName("user"))
-               .thenReturn(user("user"));
+        Mockito.when(this.auth.getUser("user")).thenReturn(user("user"));
+        Map<String, String> permission = new HashMap<>();
+        permission.put("graphspace", "NEW");
+        permission.put("permission_preset", "GS_ADMIN");
         UserEntity account = UserEntity.builder()
                                        .id("user")
                                        .name("user")
                                        .adminSpaces(
                                                Collections.singletonList("NEW"))
                                        .build();
-        account.setSuperadmin(true);
+        account.setPermissionPreset("GS_ADMIN");
+        account.setGraphspacePermissions(
+                Collections.singletonList(permission));
 
         this.service.update(this.client, account);
 
-        Mockito.verify(this.graphSpaceUsers).applySpacePreset(
-                this.client, "NEW", "user", "GS_ADMIN");
-        Mockito.verify(this.graphSpaceUsers).removeSpacePreset(
-                this.client, "OLD", "user");
-        Mockito.verify(this.auth).addSuperAdmin("user");
+        Mockito.verify(this.graphSpaceUsers).validatePermissionPresets(
+                this.client, account.getGraphspacePermissions(), "GS_ADMIN");
+        Mockito.verify(this.graphSpaceUsers).applyPermissionPresets(
+                this.client, "user", account.getGraphspacePermissions(),
+                "GS_ADMIN");
+        Mockito.verify(this.auth, Mockito.never())
+               .addSpaceAdmin(Mockito.anyString(), Mockito.anyString());
+        Mockito.verify(this.auth, Mockito.never())
+               .addSuperAdmin(Mockito.anyString());
+    }
+
+    @Test
+    public void testModernUserUpdateRollsBackProfileAndSuperAdmin() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        User previous = user("alice");
+        previous.setId("u-1");
+        previous.description("old");
+        Mockito.when(this.auth.getUser("u-1")).thenReturn(previous);
+        Mockito.when(this.auth.listSuperAdmin())
+               .thenReturn(Collections.singletonList("alice"))
+               .thenReturn(Collections.emptyList());
+        UserEntity account = UserEntity.builder()
+                                       .id("u-1")
+                                       .name("alice")
+                                       .description("new")
+                                       .build();
+        account.setPermissionPreset("GS_READ_ONLY");
+        account.setGraphspacePermissions(Collections.singletonList(
+                permission("team", "GS_READ_ONLY")));
+        RuntimeException failure = new RuntimeException("preset failed");
+        Mockito.doThrow(failure).when(this.graphSpaceUsers)
+               .applyPermissionPresets(
+                       this.client, "alice",
+                       account.getGraphspacePermissions(), "GS_READ_ONLY");
+
+        RuntimeException error = null;
+        try {
+            this.service.update(this.client, account);
+        } catch (RuntimeException e) {
+            error = e;
+        }
+
+        Assert.assertSame(failure, error);
+        Mockito.verify(this.auth).delSuperAdmin("alice");
+        Mockito.verify(this.auth).addSuperAdmin("alice");
+        ArgumentCaptor<User> updates = ArgumentCaptor.forClass(User.class);
+        Mockito.verify(this.auth, Mockito.times(2))
+               .updateUser(updates.capture());
+        Assert.assertSame(previous, updates.getAllValues().get(1));
     }
 
     @Test
@@ -312,6 +424,7 @@ public class UserServiceCompatibilityTest {
                .thenReturn(user("user"));
         Mockito.when(this.auth.listSuperAdmin())
                .thenReturn(Collections.singletonList("user"));
+        Mockito.when(this.auth.getUser("user")).thenReturn(user("user"));
         UserEntity account = UserEntity.builder()
                                        .id("user")
                                        .name("user")
@@ -326,7 +439,8 @@ public class UserServiceCompatibilityTest {
         Mockito.verify(this.auth).delSuperAdmin("user");
         Mockito.verify(this.graphSpaceUsers, Mockito.never())
                .applySpacePreset(Mockito.any(), Mockito.anyString(),
-                                 Mockito.anyString(), Mockito.anyString());
+                                 Mockito.anyString(), Mockito.anyString(),
+                                 Mockito.anyString());
     }
 
     @Test
@@ -391,6 +505,191 @@ public class UserServiceCompatibilityTest {
                                        Mockito.any(), Mockito.any());
     }
 
+    @Test
+    public void testModernScopedUserCreationRejectsEmptyGraphSpaces() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        ReflectionTestUtils.setField(this.service, "graphSpaceUserService",
+                                     new GraphSpaceUserService());
+        UserEntity user = userEntity("display-name");
+        user.setPermissionPreset("GS_READ_WRITE");
+        user.setGraphspacePermissions(Collections.emptyList());
+
+        ParameterizedException error = null;
+        try {
+            this.service.add(this.client, user);
+        } catch (ParameterizedException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertEquals("auth.permission-preset.graphspace-required",
+                            error.getMessage());
+        Mockito.verify(this.auth, Mockito.never())
+               .createUser(Mockito.any(User.class));
+    }
+
+    @Test
+    public void testModernScopedUserCreationRollsBackFailedPreset() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        Mockito.when(this.auth.createUser(Mockito.any(User.class)))
+               .thenReturn(user("created-user"));
+        UserEntity user = userEntity("display-name");
+        user.setPermissionPreset("GS_READ_ONLY");
+        user.setGraphspacePermissions(Collections.singletonList(
+                permission("team", "GS_READ_ONLY")));
+        RuntimeException failure = new RuntimeException("preset failed");
+        Mockito.doThrow(failure).when(this.graphSpaceUsers)
+               .applyPermissionPresetsForNewAccount(
+                       this.client, "user",
+                       user.getGraphspacePermissions(),
+                       "GS_READ_ONLY");
+
+        RuntimeException error = null;
+        try {
+            this.service.add(this.client, user);
+        } catch (RuntimeException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertSame(failure, error);
+        Mockito.verify(this.auth).deleteUser("created-user");
+    }
+
+    @Test
+    public void testSuperAdminGrantFailureDeletesNewAccount() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        User created = user("alice");
+        created.setId("u-1");
+        Mockito.when(this.auth.createUser(Mockito.any(User.class)))
+               .thenReturn(created);
+        RuntimeException failure = new RuntimeException("grant failed");
+        Mockito.when(this.auth.addSuperAdmin("user")).thenThrow(failure);
+        UserEntity user = userEntity("display-name");
+        user.setPermissionPreset("SUPER_ADMIN");
+        user.setSuperadmin(true);
+
+        RuntimeException error = null;
+        try {
+            this.service.add(this.client, user);
+        } catch (RuntimeException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertSame(failure, error);
+        Mockito.verify(this.auth).addSuperAdmin("user");
+        Mockito.verify(this.auth).delSuperAdmin("user");
+        Mockito.verify(this.auth).deleteUser("u-1");
+    }
+
+    @Test
+    public void testLegacyAdminGrantFailureRollsBackEarlierSpaces() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
+        User created = user("user");
+        created.setId("u-1");
+        Mockito.when(this.auth.createUser(Mockito.any(User.class)))
+               .thenReturn(created);
+        RuntimeException failure = new RuntimeException("grant failed");
+        Mockito.when(this.auth.addSpaceAdmin("user", "SECOND"))
+               .thenThrow(failure);
+        UserEntity user = userEntity("display-name");
+        user.setAdminSpaces(Arrays.asList("FIRST", "SECOND"));
+
+        RuntimeException error = null;
+        try {
+            this.service.add(this.client, user);
+        } catch (RuntimeException e) {
+            error = e;
+        }
+
+        Assert.assertSame(failure, error);
+        Mockito.verify(this.auth).delSpaceAdmin("user", "SECOND");
+        Mockito.verify(this.auth).delSpaceAdmin("user", "FIRST");
+        Mockito.verify(this.auth).deleteUser("u-1");
+    }
+
+    @Test
+    public void testModernUserCreationRejectsUnknownPreset() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        ReflectionTestUtils.setField(this.service, "graphSpaceUserService",
+                                     new GraphSpaceUserService());
+        UserEntity user = userEntity("display-name");
+        user.setPermissionPreset("UNKNOWN");
+        user.setGraphspacePermissions(Collections.emptyList());
+
+        ParameterizedException error = null;
+        try {
+            this.service.add(this.client, user);
+        } catch (ParameterizedException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertEquals("auth.permission-preset.invalid",
+                            error.getMessage());
+        Mockito.verify(this.auth, Mockito.never())
+               .createUser(Mockito.any(User.class));
+    }
+
+    @Test
+    public void testModernUserCreationRequiresPreset() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        UserEntity user = userEntity("display-name");
+
+        ParameterizedException error = null;
+        try {
+            this.service.add(this.client, user);
+        } catch (ParameterizedException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertEquals("auth.permission-preset.account-required",
+                            error.getMessage());
+        Mockito.verify(this.auth, Mockito.never())
+               .createUser(Mockito.any(User.class));
+    }
+
+    @Test
+    public void testModernUserCreationRejectsSuperAdminMismatch() {
+        Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        UserEntity superPreset = userEntity("display-name");
+        superPreset.setPermissionPreset("SUPER_ADMIN");
+
+        ParameterizedException missingFlag = null;
+        try {
+            this.service.add(this.client, superPreset);
+        } catch (ParameterizedException e) {
+            missingFlag = e;
+        }
+        Assert.assertNotNull(missingFlag);
+        Assert.assertEquals("auth.permission-preset.superadmin-mismatch",
+                            missingFlag.getMessage());
+
+        UserEntity scopedPreset = userEntity("display-name");
+        scopedPreset.setPermissionPreset("GS_READ_ONLY");
+        scopedPreset.setSuperadmin(true);
+        ParameterizedException extraFlag = null;
+        try {
+            this.service.add(this.client, scopedPreset);
+        } catch (ParameterizedException e) {
+            extraFlag = e;
+        }
+        Assert.assertNotNull(extraFlag);
+        Assert.assertEquals("auth.permission-preset.superadmin-mismatch",
+                            extraFlag.getMessage());
+        Mockito.verify(this.auth, Mockito.never())
+               .createUser(Mockito.any(User.class));
+    }
+
     private static UserEntity userEntity(String nickname) {
         return UserEntity.builder()
                          .name("user")
@@ -405,4 +704,12 @@ public class UserServiceCompatibilityTest {
         user.name(name);
         return user;
     }
+
+    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;
+    }
 }
diff --git a/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js b/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js
index aaef6bb25..239ad6578 100644
--- a/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js
+++ b/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js
@@ -111,7 +111,7 @@ describe('auth API contract', () => {
             '/graphspaces/A%2FB/auth/users/spaceadmin/u%2F1'],
         ['removeSpaceAdmin', ['A/B', 'u/1'], 'delete',
             '/graphspaces/A%2FB/auth/users/spaceadmin/u%2F1'],
-        ['setSpacePreset', ['A/B', 'u/1', 'GS_READ_ONLY'], 'put-preset',
+        ['setSpacePreset', ['A/B', 'u/1', 'alice', 'GS_READ_ONLY'], 'put-preset',
             '/graphspaces/A%2FB/auth/users/u%2F1/preset'],
         ['addSpaceMember', ['A/B', {user_id: 'u'}], 'post',
             '/graphspaces/A%2FB/auth/users'],
@@ -166,7 +166,11 @@ describe('auth API contract', () => {
         }
         else if (verb === 'put-preset') {
             expect(mockRequest.put).toHaveBeenCalledWith(
-                route, {permission_preset: args[2]}, config
+                route, {
+                    user_id: args[1],
+                    username: args[2],
+                    permission_preset: args[3],
+                }, config
             );
         }
         else {
diff --git a/hugegraph-hubble/hubble-fe/src/api/auth.js b/hugegraph-hubble/hubble-fe/src/api/auth.js
index 5b7075c89..6a7c97506 100644
--- a/hugegraph-hubble/hubble-fe/src/api/auth.js
+++ b/hugegraph-hubble/hubble-fe/src/api/auth.js
@@ -109,8 +109,10 @@ const removeSpaceAdmin = (graphspace, id, config) => {
     return request.delete(scopedAuthPath(graphspace, 'users/spaceadmin', id), undefined, config);
 };
 
-const setSpacePreset = (graphspace, id, preset, config) => {
+const setSpacePreset = (graphspace, id, username, preset, config) => {
     return request.put(`${scopedAuthPath(graphspace, 'users', id)}/preset`, {
+        user_id: id === username ? undefined : id,
+        username,
         permission_preset: preset,
     }, config);
 };
diff --git a/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js
index b539b7f42..febdbdc95 100644
--- a/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js
+++ b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js
@@ -23,6 +23,7 @@ const loadResponseHandlers = modulePath => {
     const messageError = jest.fn();
     const modalWarning = jest.fn();
     const clearLogin = jest.fn();
+    const isLogoutTransition = jest.fn(() => false);
     const instance = {
         interceptors: {
             request: {
@@ -56,6 +57,7 @@ const loadResponseHandlers = modulePath => {
     }));
     jest.doMock('../utils/user', () => ({
         clearLogin,
+        isLogoutTransition,
     }));
 
     const request = require(modulePath).default;
@@ -65,6 +67,7 @@ const loadResponseHandlers = modulePath => {
         messageError,
         modalWarning,
         clearLogin,
+        isLogoutTransition,
         instance,
         request,
     };
@@ -78,6 +81,7 @@ describe.each(['./request'])('%s error semantics', modulePath => {
             search: '?x=1',
             hash: '#result',
             href: '',
+            replace: jest.fn(),
         };
     });
 
@@ -258,6 +262,49 @@ describe.each(['./request'])('%s error semantics', modulePath => {
         expect(instance.delete).not.toHaveBeenCalled();
     });
 
+    it('does not restore the previous route when logout receives business 401', async () => {
+        const {resolve, clearLogin, isLogoutTransition}
+            = loadResponseHandlers(modulePath);
+        isLogoutTransition.mockReturnValue(true);
+        const response = {
+            status: 200,
+            config: {url: '/auth/logout'},
+            data: {
+                status: 401,
+                message: 'Unauthorized',
+            },
+        };
+
+        await expect(resolve(response)).rejects.toBe(response);
+        expect(clearLogin).toHaveBeenCalledTimes(1);
+        expect(sessionStorage.getItem('redirect')).toBeNull();
+        expect(window.location.replace).toHaveBeenCalledWith('/login');
+        expect(window.location.href).toBe('');
+    });
+
+    it('does not restore the previous route when a concurrent status returns HTTP 401',
+        async () => {
+            const {reject, clearLogin, isLogoutTransition}
+                = loadResponseHandlers(modulePath);
+            isLogoutTransition.mockReturnValue(true);
+            const error = {
+                config: {url: '/auth/status'},
+                response: {
+                    status: 401,
+                    data: {
+                        status: 401,
+                        message: 'Unauthorized',
+                    },
+                },
+            };
+
+            await expect(reject(error)).rejects.toBe(error);
+            expect(clearLogin).toHaveBeenCalledTimes(1);
+            expect(sessionStorage.getItem('redirect')).toBeNull();
+            expect(window.location.replace).toHaveBeenCalledWith('/login');
+            expect(window.location.href).toBe('');
+        });
+
     it.each([401, 403])(
         'shows business %s from login without redirecting',
         async status => {
diff --git a/hugegraph-hubble/hubble-fe/src/api/request.js b/hugegraph-hubble/hubble-fe/src/api/request.js
index 3f3432e1d..1b63ae16e 100644
--- a/hugegraph-hubble/hubble-fe/src/api/request.js
+++ b/hugegraph-hubble/hubble-fe/src/api/request.js
@@ -40,7 +40,13 @@ const parseResponse = (data, headers) => {
 };
 
 const redirectToLogin = () => {
+    const logoutTransition = user.isLogoutTransition();
     user.clearLogin();
+    if (logoutTransition) {
+        sessionStorage.removeItem('redirect');
+        window.location.replace('/login');
+        return;
+    }
     if (window.location.pathname !== '/login') {
         const redirect = `${window.location.pathname}${window.location.search}`
                          + window.location.hash;
diff --git a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js
index e9f09b336..92ae8b41b 100644
--- a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js
+++ b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js
@@ -156,7 +156,17 @@ const GraphContextSwitcher = () => {
             })
             .catch(error => {
                 if (!cancelled) {
-                    setErrors(value => ({...value, graphs: errorKind(error)}));
+                    const kind = errorKind(error);
+                    if (pdEnabled && kind === 'forbidden') {
+                        clearWorkbenchGraphContext(localStorage);
+                        setContext({});
+                        setGraphs([]);
+                        setErrors(value => ({...value, graphs: false}));
+                        navigate('/navigation', {replace: true});
+                    }
+                    else {
+                        setErrors(value => ({...value, graphs: kind}));
+                    }
                     setLoading(value => ({...value, graphs: false}));
                 }
             });
@@ -164,7 +174,7 @@ const GraphContextSwitcher = () => {
         return () => {
             cancelled = true;
         };
-    }, [context.graphspace, navigate, reloadTokens.graphs]);
+    }, [context.graphspace, navigate, pdEnabled, reloadTokens.graphs]);
 
     useEffect(() => {
         if (!pdEnabled || loading.graphspaces || errors.graphspaces || !context.graphspace || (
@@ -173,18 +183,10 @@ const GraphContextSwitcher = () => {
             return;
         }
 
-        const graphspace = graphspaces[0]?.name;
-        const nextContext = graphspace ? {graphspace} : {};
-        setContext(nextContext);
+        setContext({});
         setGraphs([]);
-        if (graphspace) {
-            writeWorkbenchGraphContext(localStorage, nextContext);
-            navigate(`/graphspace/${encodeURIComponent(graphspace)}`, {replace: true});
-        }
-        else {
-            clearWorkbenchGraphContext(localStorage);
-            navigate('/graphspace', {replace: true});
-        }
+        clearWorkbenchGraphContext(localStorage);
+        navigate('/navigation', {replace: true});
     }, [
         context.graphspace,
         errors.graphspaces,
diff --git a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js
index 70efc86ca..8e1d764df 100644
--- a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js
+++ b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js
@@ -181,7 +181,7 @@ describe('GraphContextSwitcher', () => {
         expect(screen.getByRole('option', {name: 'demo_space'})).toBeInTheDocument();
     });
 
-    test('replaces a missing GraphSpace and never reuses its graph', async () => {
+    test('clears a missing GraphSpace instead of guessing another one', async () => {
         sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true}));
         api.manage.getGraphSpaceList.mockResolvedValueOnce({
             status: 200,
@@ -191,14 +191,12 @@ describe('GraphContextSwitcher', () => {
 
         await waitFor(() => {
             expect(screen.getByRole('combobox', {name: 'workbench.context.graphspace'}))
-                .toHaveValue('space_b');
+                .toHaveValue('');
         });
         expect(screen.getByRole('combobox', {name: 'workbench.context.graph'}))
             .not.toHaveValue('old_graph');
-        expect(screen.getByText('/graphspace/space_b')).toBeInTheDocument();
-        expect(JSON.parse(localStorage.getItem('hubble_workbench_graph_context'))).toEqual({
-            graphspace: 'space_b',
-        });
+        expect(screen.getByText('/navigation')).toBeInTheDocument();
+        expect(localStorage.getItem('hubble_workbench_graph_context')).toBeNull();
     });
 
     test('clears a deep-linked graph that does not belong to its GraphSpace', async () => {
@@ -308,6 +306,26 @@ describe('GraphContextSwitcher', () => {
         })).not.toBeInTheDocument();
     });
 
+    test('clears a forbidden PD GraphSpace left by a previous account', async () => {
+        sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true}));
+        localStorage.setItem('hubble_workbench_graph_context', JSON.stringify({
+            graphspace: 'space_a',
+            graph: 'graph_a',
+        }));
+        api.manage.getGraphList.mockResolvedValueOnce({status: 403, data: null});
+        renderSwitcher('/gremlin/space_a/graph_a');
+
+        await waitFor(() => {
+            expect(screen.getByText('/navigation')).toBeInTheDocument();
+        });
+        expect(localStorage.getItem('hubble_workbench_graph_context')).toBeNull();
+        expect(screen.getByRole('combobox', {
+            name: 'workbench.context.graphspace',
+        })).toHaveValue('');
+        expect(screen.queryByText('workbench.context.graphs_forbidden'))
+            .not.toBeInTheDocument();
+    });
+
     test('graph success cannot erase a concurrent GraphSpace failure', async () => {
         sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true}));
         api.manage.getGraphSpaceList.mockResolvedValueOnce({status: 500, data: null});
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 0f095e0bd..638f02974 100644
--- a/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js
+++ b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js
@@ -34,8 +34,17 @@ import {
 import {useAuthContext} from '../../auth/AuthContext';
 import {isAuthEnabled} from '../../utils/config';
 
+const isUnauthorized = error => {
+    return error?.status === 401
+           || error?.data?.status === 401
+           || error?.response?.status === 401
+           || error?.response?.data?.status === 401
+           || error?.message?.includes('status code 401');
+};
+
 const Topbar = () => {
     const userInfo = user.getUser();
+    const userId = userInfo?.id;
     const location = useLocation();
     const {t} = useTranslation();
     const {context: authContext} = useAuthContext();
@@ -51,13 +60,13 @@ const Topbar = () => {
     useEffect(() => {
         let cancelled = false;
 
-        if (isAuthEnabled() && (!userInfo || !userInfo.id)) {
+        if (isAuthEnabled() && !userId) {
             return undefined;
         }
 
         api.auth.status()
             .then(res => {
-                if (!cancelled && res.status === 401) {
+                if (!cancelled && !user.isLogoutTransition() && res.status === 401) {
                     redirectToLogin();
                 }
             })
@@ -68,27 +77,37 @@ const Topbar = () => {
         return () => {
             cancelled = true;
         };
-    }, [redirectToLogin, userInfo]);
+    }, [redirectToLogin, userId]);
 
-    if (isAuthEnabled() && (!userInfo || !userInfo.id)) {
-        redirectToLogin();
-    }
+    useEffect(() => {
+        if (isAuthEnabled() && !userId && !user.isLogoutTransition()) {
+            redirectToLogin();
+        }
+    }, [redirectToLogin, userId]);
 
     const showShortcutHelp = useCallback(() => {
         window.dispatchEvent(new CustomEvent('hubble:shortcut-help'));
     }, []);
 
     const logout = useCallback(() => {
-
-        api.auth.logout().then(res => {
-            if (res.status === 200) {
-                sessionStorage.removeItem('redirect');
-                clearPersistedAlgorithmFormsForUser();
-                user.clearLogin();
-                message.success(t('Topbar.exit.success'));
-                window.location.replace('/login');
-            }
-        });
+        user.beginLogoutTransition();
+        api.auth.logout()
+            .then(res => {
+                if (res.status === 200) {
+                    sessionStorage.removeItem('redirect');
+                    clearPersistedAlgorithmFormsForUser();
+                    user.clearLogin();
+                    message.success(t('Topbar.exit.success'));
+                    window.location.replace('/login');
+                    return;
+                }
+                user.endLogoutTransition();
+            })
+            .catch(error => {
+                if (!isUnauthorized(error)) {
+                    user.endLogoutTransition();
+                }
+            });
     }, [t]);
 
     const userMenu = {
diff --git a/hugegraph-hubble/hubble-fe/src/components/Topbar/topbar-request-error.test.js b/hugegraph-hubble/hubble-fe/src/components/Topbar/topbar-request-error.test.js
index 9430f7250..dfdbc4aaf 100644
--- a/hugegraph-hubble/hubble-fe/src/components/Topbar/topbar-request-error.test.js
+++ b/hugegraph-hubble/hubble-fe/src/components/Topbar/topbar-request-error.test.js
@@ -20,6 +20,7 @@ import {fireEvent, render, screen, waitFor} from '@testing-library/react';
 import {MemoryRouter} from 'react-router-dom';
 import Topbar from './index.ant';
 import * as api from '../../api/index';
+import * as user from '../../utils/user';
 
 const mockUseAuthContext = jest.fn();
 
@@ -116,6 +117,7 @@ jest.mock('antd', () => {
 describe('Topbar request errors', () => {
     beforeEach(() => {
         jest.clearAllMocks();
+        user.endLogoutTransition();
         api.manage.getGraphList.mockResolvedValue({
             status: 200,
             data: {records: [{name: 'hugegraph'}]},
@@ -276,9 +278,9 @@ describe('Topbar request errors', () => {
         api.auth.status.mockResolvedValue({status: 200});
         api.auth.logout.mockResolvedValue({status: 200});
 
-        render(
+        const view = render(
             <MemoryRouter
-                initialEntries={['/navigation']}
+                initialEntries={['/graphspace/DEFAULT']}
                 future={{v7_startTransition: true, v7_relativeSplatPath: true}}
             >
                 <Topbar />
@@ -289,5 +291,48 @@ describe('Topbar request errors', () => {
 
         await waitFor(() => expect(api.auth.logout).toHaveBeenCalledTimes(1));
         expect(window.location.replace).toHaveBeenCalledWith('/login');
+        view.rerender(
+            <MemoryRouter
+                initialEntries={['/graphspace/DEFAULT']}
+                future={{v7_startTransition: true, v7_relativeSplatPath: true}}
+            >
+                <Topbar />
+            </MemoryRouter>
+        );
+        expect(sessionStorage.getItem('redirect')).toBeNull();
+        expect(window.location.href).toBe('http://localhost/navigation');
+        expect(user.isLogoutTransition()).toBe(true);
+    });
+
+    it.each([
+        ['HTTP', {response: {status: 401, data: {status: 401}}}],
+        ['business', {status: 200, data: {status: 401}}],
+    ])('does not restore the old route after a logout %s 401', async (_, error) => {
+        api.auth.status.mockResolvedValue({status: 200});
+        api.auth.logout.mockRejectedValue(error);
+
+        const view = render(
+            <MemoryRouter
+                initialEntries={['/graphspace/DEFAULT']}
+                future={{v7_startTransition: true, v7_relativeSplatPath: true}}
+            >
+                <Topbar />
+            </MemoryRouter>
+        );
+
+        fireEvent.click(screen.getByRole('button', {name: 'Topbar.exit.name'}));
+        await waitFor(() => expect(api.auth.logout).toHaveBeenCalledTimes(1));
+        view.rerender(
+            <MemoryRouter
+                initialEntries={['/graphspace/DEFAULT']}
+                future={{v7_startTransition: true, v7_relativeSplatPath: true}}
+            >
+                <Topbar />
+            </MemoryRouter>
+        );
+
+        expect(user.isLogoutTransition()).toBe(true);
+        expect(sessionStorage.getItem('redirect')).toBeNull();
+        expect(window.location.href).toBe('http://localhost/navigation');
     });
 });
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index 8e25be843..df60813f8 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -32,8 +32,8 @@ import {
     PERMISSION_PRESETS,
     toPermissionPayload,
 } from './permissionPresets';
+import {loadAllPages, PAGE_ERROR_CONFIG} from './pagedRecords';
 
-const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
 const DEFAULT_ALLOWED_OPERATIONS = {create: true, edit: true, auth: true};
 const PRESERVE_PERMISSIONS = 'PRESERVE_PERMISSIONS';
 const permissionPresetChanged = (prev, next) => prev.permission_preset !== next.permission_preset;
@@ -44,6 +44,8 @@ const toProfilePayload = values => ({
     user_description: values.user_description,
 });
 
+const loadAllGraphspaces = () => loadAllPages(api.manage.getGraphSpaceList);
+
 const HelpLabel = ({t, labelKey}) => (
     <FormHelpLabel
         label={t(labelKey)}
@@ -189,7 +191,7 @@ const EditLayer = ({
         detailRequest.current = request;
         setGraphspaceList([]);
         if (op !== 'detail' && (permissionPresetsSupported || op === 'auth')) {
-            api.manage.getGraphSpaceList(undefined, PAGE_ERROR_CONFIG).then(res => {
+            loadAllGraphspaces().then(res => {
                 if (detailRequest.current !== request) {
                     return;
                 }
@@ -426,6 +428,7 @@ const EditLayer = ({
                                                                 />
                                                             )}
                                                             name="graphspaces"
+                                                            rules={[rules.required()]}
                                                         >
                                                             <Select options={graphspaceList} mode="multiple" />
                                                         </Form.Item>
@@ -452,6 +455,7 @@ const EditLayer = ({
                                     <Form.Item
                                         label={<HelpLabel t={t} labelKey='account.form.graphspaces' />}
                                         name="graphspaces"
+                                        rules={[rules.required()]}
                                     >
                                         <Select options={graphspaceList} mode="multiple" />
                                     </Form.Item>
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
index 8dd3fa1b0..a1c53f90f 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
@@ -36,9 +36,8 @@ import * as api from '../../api';
 import TableHeader from '../../components/TableHeader';
 import {useAuthContext} from '../../auth/AuthContext';
 import {PERMISSION_PRESETS} from './permissionPresets';
+import {loadAllPages, PAGE_ERROR_CONFIG} from './pagedRecords';
 
-const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
-const PAGE_PARAMS = {query: '', page_no: 1, page_size: 200};
 const responseRecords = response => response?.data?.records ?? [];
 const showMutationError = (error, t) => {
     const response = error?.response ?? error;
@@ -205,7 +204,7 @@ const SpaceAccess = () => {
         spacesRequest.current = token;
         setSpacesLoading(true);
         setSpacesError(false);
-        api.manage.getGraphSpaceList(PAGE_PARAMS, PAGE_ERROR_CONFIG)
+        loadAllPages(api.manage.getGraphSpaceList, {params: {query: ''}})
             .then(response => {
                 if (spacesRequest.current !== token) {
                     return;
@@ -235,11 +234,13 @@ const SpaceAccess = () => {
 
     const spaces = scopes.all_graphspaces ? allSpaces : scopedSpaces;
     const graphSpace = spaces.includes(selectedSpace) ? selectedSpace : spaces[0];
-    const loadMembers = useCallback(space => api.auth.getSpaceMembers(
-        space, PAGE_PARAMS, PAGE_ERROR_CONFIG
+    const loadMembers = useCallback(space => loadAllPages(
+        (params, config) => api.auth.getSpaceMembers(space, params, config),
+        {params: {query: ''}}
     ), []);
-    const loadAdmins = useCallback(space => api.auth.getSpaceAdmins(
-        space, PAGE_PARAMS, PAGE_ERROR_CONFIG
+    const loadAdmins = useCallback(space => loadAllPages(
+        (params, config) => api.auth.getSpaceAdmins(space, params, config),
+        {params: {query: ''}}
     ), []);
     const members = useScopedResource(
         graphSpace, contextVersion, loadMembers, responseRecords
@@ -287,6 +288,7 @@ const SpaceAccess = () => {
     const openMember = useCallback(row => {
         memberForm.setFieldsValue({
             user_id: row?.user_id,
+            username: row?.user_name,
             permission_preset: rolesPreset(row?.roles),
         });
         setMemberDialog(row ?? {});
@@ -297,7 +299,11 @@ const SpaceAccess = () => {
     }, [memberForm]);
     const submitMember = useCallback(values => {
         runMutation(
-            () => api.auth.setSpacePreset(graphSpace, values.user_id, values.permission_preset,
+            () => api.auth.setSpacePreset(
+                graphSpace,
+                values.user_id ?? values.username,
+                values.username,
+                values.permission_preset,
                 PAGE_ERROR_CONFIG
             ),
             closeMember
@@ -431,13 +437,28 @@ const SpaceAccess = () => {
                 destroyOnClose
             >
                 <Form form={memberForm} layout="vertical" onFinish={submitMember}>
-                    <Form.Item
-                        name="user_id"
-                        label={t('account.space_access.member.id')}
-                        rules={[{required: true}]}
-                    >
-                        <Input disabled={Boolean(memberDialog?.user_id)} />
-                    </Form.Item>
+                    {memberDialog?.user_id ? (
+                        <>
+                            <Form.Item
+                                name="user_id"
+                                label={t('account.space_access.member.id')}
+                                rules={[{required: true}]}
+                            >
+                                <Input disabled />
+                            </Form.Item>
+                            <Form.Item name="username" hidden>
+                                <Input />
+                            </Form.Item>
+                        </>
+                    ) : (
+                        <Form.Item
+                            name="username"
+                            label={t('account.space_access.member.name')}
+                            rules={[{required: true}]}
+                        >
+                            <Input />
+                        </Form.Item>
+                    )}
                     <Form.Item
                         name="permission_preset"
                         label={t('account.space_access.member.roles')}
@@ -461,5 +482,5 @@ const SpaceAccess = () => {
     );
 };
 
-export {rolesPreset};
+export {loadAllPages, rolesPreset};
 export default SpaceAccess;
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 bcbcade6c..2d72aaa7b 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
@@ -24,7 +24,7 @@ import {
     waitFor,
     within,
 } from '@testing-library/react';
-import SpaceAccess, {rolesPreset} from './SpaceAccess';
+import SpaceAccess, {loadAllPages, rolesPreset} from './SpaceAccess';
 import * as api from '../../api';
 
 let mockAuthContext;
@@ -115,6 +115,90 @@ test('keeps graphspace administrator selected with member roles', () => {
     ])).toBeNull();
 });
 
+test('loads every page without silently truncating member-management data', async () => {
+    const firstPage = Array.from({length: 500}, (_, index) => ({id: index}));
+    const request = jest.fn()
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: firstPage, total: '501'},
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: [{id: 500}], total: 501},
+        });
+
+    const response = await loadAllPages(request, {params: {query: ''}});
+
+    expect(response.data.records).toHaveLength(501);
+    expect(request).toHaveBeenNthCalledWith(1, {
+        query: '',
+        page_no: 1,
+        page_size: 500,
+    }, expect.any(Object));
+    expect(request).toHaveBeenNthCalledWith(2, {
+        query: '',
+        page_no: 2,
+        page_size: 500,
+    }, expect.any(Object));
+});
+
+test('continues after short pages while declared records remain', async () => {
+    const page = offset => Array.from({length: 200}, (_, index) => ({
+        id: offset + index,
+    }));
+    const request = jest.fn()
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: page(0), total: 501},
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: page(200), total: 501},
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: page(400).slice(0, 101), total: 501},
+        });
+
+    const response = await loadAllPages(request);
+
+    expect(response.data.records).toHaveLength(501);
+    expect(request).toHaveBeenCalledTimes(3);
+});
+
+test('treats blank total as absent instead of zero', async () => {
+    const firstPage = Array.from({length: 500}, (_, index) => ({id: index}));
+    const request = jest.fn()
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: firstPage, total: '   '},
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: [{id: 500}], total: '   '},
+        });
+
+    const response = await loadAllPages(request);
+
+    expect(response.data.records).toHaveLength(501);
+    expect(request).toHaveBeenCalledTimes(2);
+});
+
+test('fails when a paged response ends before its declared total', async () => {
+    const request = jest.fn()
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: [{id: 0}], total: 2},
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: [], total: 2},
+        });
+
+    await expect(loadAllPages(request))
+        .rejects.toThrow('Record list ended before its declared total');
+});
+
 test('shows legacy role names without treating them as presets', async () => {
     setResponses({
         members: [{
@@ -248,8 +332,7 @@ test('submits only the selected preset when adding a member', async () => {
     fireEvent.click(screen.getByRole('button', {name: 'OK'}));
 
     await waitFor(() => expect(api.auth.setSpacePreset).toHaveBeenCalledWith(
-        'SPACE_A', 'bob', 'GS_READ_WRITE', expect.any(Object)
-    ));
+        'SPACE_A', 'bob', 'bob', 'GS_READ_WRITE', expect.any(Object)));
 });
 
 test('uses the preset API for GS admin', async () => {
@@ -277,8 +360,7 @@ test('uses the preset API for GS admin', async () => {
     fireEvent.click(screen.getByRole('button', {name: 'OK'}));
 
     await waitFor(() => expect(api.auth.setSpacePreset).toHaveBeenCalledWith(
-        'SPACE_A', 'bob', 'GS_ADMIN', expect.any(Object)
-    ));
+        'SPACE_A', 'bob', 'bob', 'GS_ADMIN', expect.any(Object)));
     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 4521c33c5..14c5e20f2 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
@@ -213,6 +213,10 @@ test('preserves mixed GraphSpace permissions on profile edit', async () => {
 test('ignores a second account mutation while the first submit is pending', async () => {
     const detailRequest = deferred();
     const mutation = deferred();
+    api.manage.getGraphSpaceList.mockResolvedValue({
+        status: 200,
+        data: {records: [{name: 'SPACE'}]},
+    });
     api.auth.getUserInfo.mockReturnValue(detailRequest.promise);
     api.auth.updateAdminspace.mockReturnValue(mutation.promise);
     render(<EditLayer {...props} data={{id: 'A'}} op='auth' />);
@@ -223,7 +227,7 @@ test('ignores a second account mutation while the first submit is pending', asyn
 
     await act(async () => detailRequest.resolve({
         status: 200,
-        data: {user_name: 'alice', adminSpaces: []},
+        data: {user_name: 'alice', adminSpaces: ['SPACE']},
     }));
     fireEvent.click(submit);
     fireEvent.click(submit);
@@ -281,11 +285,100 @@ test('loads graphspaces into the visible create account form', async () => {
         status: 200,
         data: {records: [{name: 'analytics'}]},
     }));
+    expect(api.manage.getGraphSpaceList).toHaveBeenCalledWith({
+        page_no: 1,
+        page_size: 500,
+    }, {suppressBusinessErrorToast: true});
     fireEvent.mouseDown(screen.getAllByRole('combobox')[1]);
 
     expect(await screen.findByRole('option', {name: 'analytics'})).toBeInTheDocument();
 });
 
+test('loads every GraphSpace page for permission assignment', async () => {
+    api.manage.getGraphSpaceList
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: [{name: 'first'}], total: '2'},
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: [{name: 'second'}], total: 2},
+        });
+
+    render(<EditLayer {...props} data={{}} op='create' />);
+    await waitFor(() =>
+        expect(api.manage.getGraphSpaceList).toHaveBeenCalledTimes(2)
+    );
+    fireEvent.mouseDown(screen.getAllByRole('combobox')[1]);
+
+    expect(await screen.findByRole('option', {name: 'first'}))
+        .toBeInTheDocument();
+    expect(await screen.findByRole('option', {name: 'second'}))
+        .toBeInTheDocument();
+});
+
+test('continues after a full GraphSpace page without total', async () => {
+    const firstPage = Array.from({length: 500}, (_, index) => ({
+        name: `space-${index}`,
+    }));
+    api.manage.getGraphSpaceList
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: firstPage},
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {records: [{name: 'last'}]},
+        });
+
+    render(<EditLayer {...props} data={{}} op='create' />);
+
+    await waitFor(() =>
+        expect(api.manage.getGraphSpaceList).toHaveBeenCalledTimes(2)
+    );
+});
+
+test.each([null, ''])(
+    'continues after a full GraphSpace page with empty total %p',
+    async total => {
+        const firstPage = Array.from({length: 500}, (_, index) => ({
+            name: `space-${index}`,
+        }));
+        api.manage.getGraphSpaceList
+            .mockResolvedValueOnce({
+                status: 200,
+                data: {records: firstPage, total},
+            })
+            .mockResolvedValueOnce({
+                status: 200,
+                data: {records: [{name: 'last'}]},
+            });
+
+        render(<EditLayer {...props} data={{}} op='create' />);
+
+        await waitFor(() =>
+            expect(api.manage.getGraphSpaceList).toHaveBeenCalledTimes(2)
+        );
+    }
+);
+
+test('accepts exactly 10000 GraphSpaces without a declared total', async () => {
+    api.manage.getGraphSpaceList.mockImplementation(({page_no: pageNo}) => {
+        const records = pageNo <= 20
+            ? Array.from({length: 500}, (_, index) => ({
+                name: `space-${pageNo}-${index}`,
+            }))
+            : [];
+        return Promise.resolve({status: 200, data: {records}});
+    });
+
+    render(<EditLayer {...props} data={{}} op='create' />);
+
+    await waitFor(() =>
+        expect(api.manage.getGraphSpaceList).toHaveBeenCalledTimes(21)
+    );
+});
+
 test('requires an explicit password when creating an account', async () => {
     render(<EditLayer {...props} data={{}} op='create' />);
     await act(async () => undefined);
@@ -295,6 +388,49 @@ test('requires an explicit password when creating an account', async () => {
     )).toBeRequired();
 });
 
+test('requires a GraphSpace when creating a scoped account', async () => {
+    api.auth.addUser.mockResolvedValue({status: 200});
+    render(<EditLayer {...props} data={{}} op='create' />);
+    await act(async () => undefined);
+
+    fireEvent.change(screen.getByPlaceholderText(
+        'account.form.id_placeholder'
+    ), {target: {value: 'alice'}});
+    fireEvent.change(screen.getByPlaceholderText(
+        'account.form.default_password_placeholder'
+    ), {target: {value: 'secret'}});
+    fireEvent.mouseDown(screen.getAllByRole('combobox')[0]);
+    fireEvent.click(screen.getByText(
+        'account.permission_preset.GS_READ_WRITE'
+    ));
+    fireEvent.click(document.querySelector(
+        '.ant-modal-footer .ant-btn-primary'
+    ));
+
+    await waitFor(() =>
+        expect(document.querySelector('.ant-form-item-has-error'))
+            .not.toBeNull()
+    );
+    expect(api.auth.addUser).not.toHaveBeenCalled();
+});
+
+test('requires a GraphSpace when granting space administration', async () => {
+    api.auth.updateAdminspace.mockResolvedValue({status: 200});
+    render(<EditLayer {...props} data={{id: 'A'}} op='auth' />);
+    const submit = document.querySelector(
+        '.ant-modal-footer .ant-btn-primary'
+    );
+    await waitFor(() => expect(submit).not.toBeDisabled());
+
+    fireEvent.click(submit);
+
+    await waitFor(() =>
+        expect(document.querySelector('.ant-form-item-has-error'))
+            .not.toBeNull()
+    );
+    expect(api.auth.updateAdminspace).not.toHaveBeenCalled();
+});
+
 test('keeps a specific account creation error visible in the form', async () => {
     api.auth.addUser.mockResolvedValue({
         status: 400,
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/pagedRecords.js b/hugegraph-hubble/hubble-fe/src/pages/Account/pagedRecords.js
new file mode 100644
index 000000000..627e15f3b
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/pagedRecords.js
@@ -0,0 +1,74 @@
+/*
+ *
+ * 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 PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
+const PAGE_SIZE = 500;
+const MAX_RECORDS = 10_000;
+
+const loadAllPages = async (request, {params = {}, config = PAGE_ERROR_CONFIG} = {}) => {
+    const records = [];
+    let pageNo = 1;
+    let total;
+
+    while (records.length <= MAX_RECORDS) {
+        const response = await request({
+            ...params,
+            page_no: pageNo,
+            page_size: PAGE_SIZE,
+        }, config);
+        if (response?.status !== 200) {
+            return response;
+        }
+        const pageRecords = response?.data?.records ?? [];
+        if (records.length + pageRecords.length > MAX_RECORDS) {
+            throw new Error(`Record list exceeds ${MAX_RECORDS}`);
+        }
+        records.push(...pageRecords);
+
+        const rawTotal = response.data?.total;
+        const normalizedTotal = typeof rawTotal === 'string'
+            ? rawTotal.trim()
+            : rawTotal;
+        if (normalizedTotal !== undefined
+            && normalizedTotal !== null
+            && normalizedTotal !== '') {
+            const declaredTotal = Number(normalizedTotal);
+            if (Number.isFinite(declaredTotal) && declaredTotal >= 0) {
+                if (declaredTotal > MAX_RECORDS) {
+                    throw new Error(`Record list exceeds ${MAX_RECORDS}`);
+                }
+                total = declaredTotal;
+            }
+        }
+        if (total !== undefined && records.length < total
+            && pageRecords.length === 0) {
+            throw new Error('Record list ended before its declared total');
+        }
+        if ((total !== undefined && records.length >= total)
+            || (total === undefined && pageRecords.length < PAGE_SIZE)) {
+            return {
+                status: 200,
+                data: {records, total: total ?? records.length},
+            };
+        }
+        pageNo += 1;
+    }
+    throw new Error(`Record list exceeds ${MAX_RECORDS}`);
+};
+
+export {loadAllPages, PAGE_ERROR_CONFIG};
diff --git a/hugegraph-hubble/hubble-fe/src/pages/My/index.js b/hugegraph-hubble/hubble-fe/src/pages/My/index.js
index e3ac4896a..cad01a205 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/My/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/My/index.js
@@ -24,9 +24,11 @@ import style from './index.module.scss';
 import EditLayer from './EditLayer';
 import * as api from '../../api';
 import * as rules from '../../utils/rules';
+import {useAuthContext} from '../../auth/AuthContext';
 
 const My = () => {
     const {t} = useTranslation();
+    const {context} = useAuthContext();
     const [editLayerVisible, setEditLayerVisible] = useState(false);
     const [changePass, setChangePass] = useState(false);
     const [refresh, setRefresh] = useState(false);
@@ -63,6 +65,9 @@ const My = () => {
         : (Array.isArray(data.adminSpaces) && data.adminSpaces.length > 0
             ? data.adminSpaces.join(', ')
             : '');
+    const accountActions = context?.actions?.account ?? [];
+    const canUpdateProfile = accountActions.includes('update');
+    const canChangePassword = accountActions.includes('change_password');
 
     const handleForm = useCallback(async () => {
         try {
@@ -197,20 +202,24 @@ const My = () => {
                         </div>
                         {!changePass && (
                             <Space className={style.profileActions} wrap>
-                                <Button
-                                    icon={<EditOutlined aria-hidden='true' />}
-                                    onClick={handleShowLayer}
-                                    disabled={spinning || profileError}
-                                >
-                                    {t('common.action.edit')}
-                                </Button>
-                                <Button
-                                    icon={<LockOutlined aria-hidden='true' />}
-                                    onClick={handleChange}
-                                    disabled={spinning || profileError}
-                                >
-                                    {t('my.edit.title')}
-                                </Button>
+                                {canUpdateProfile && (
+                                    <Button
+                                        icon={<EditOutlined aria-hidden='true' />}
+                                        onClick={handleShowLayer}
+                                        disabled={spinning || profileError}
+                                    >
+                                        {t('common.action.edit')}
+                                    </Button>
+                                )}
+                                {canChangePassword && (
+                                    <Button
+                                        icon={<LockOutlined aria-hidden='true' />}
+                                        onClick={handleChange}
+                                        disabled={spinning || profileError}
+                                    >
+                                        {t('my.edit.title')}
+                                    </Button>
+                                )}
                             </Space>
                         )}
                     </header>
diff --git a/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js b/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js
index 25999455f..a530bbe62 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js
@@ -21,10 +21,16 @@ import userEvent from '@testing-library/user-event';
 import My from './index';
 import * as api from '../../api';
 
+let mockAuthContext = null;
+
 jest.mock('react-i18next', () => ({
     useTranslation: () => ({t: key => key}),
 }));
 
+jest.mock('../../auth/AuthContext', () => ({
+    useAuthContext: () => ({context: mockAuthContext}),
+}));
+
 jest.mock('../../api', () => ({
     auth: {
         getPersonal: jest.fn(),
@@ -43,6 +49,11 @@ jest.mock('../../utils/rules', () => ({
 
 beforeEach(() => {
     jest.clearAllMocks();
+    mockAuthContext = {
+        actions: {
+            account: ['read', 'update', 'change_password'],
+        },
+    };
     window.matchMedia = jest.fn().mockImplementation(query => ({
         matches: false,
         media: query,
@@ -52,6 +63,25 @@ beforeEach(() => {
     api.auth.status.mockResolvedValue({status: 200, data: {level: 'ADMIN'}});
 });
 
+test('keeps legacy profile read and password actions without edit', async () => {
+    mockAuthContext = {
+        actions: {account: ['read', 'change_password']},
+    };
+    api.auth.getPersonal.mockResolvedValue({
+        status: 200,
+        data: {user_name: 'alice'},
+    });
+
+    render(<My />);
+
+    expect(await screen.findByRole('heading', {name: 'alice'}))
+        .toBeInTheDocument();
+    expect(screen.queryByRole('button', {name: 'common.action.edit'}))
+        .not.toBeInTheDocument();
+    expect(screen.getByRole('button', {name: 'my.edit.title'}))
+        .toBeInTheDocument();
+});
+
 const deferred = () => {
     let resolve;
     const promise = new Promise(resolvePromise => {
diff --git a/hugegraph-hubble/hubble-fe/src/utils/user.js b/hugegraph-hubble/hubble-fe/src/utils/user.js
index 3967784c3..ec5e10bc7 100644
--- a/hugegraph-hubble/hubble-fe/src/utils/user.js
+++ b/hugegraph-hubble/hubble-fe/src/utils/user.js
@@ -18,6 +18,7 @@
 
 const key = 'user_'; // 防止缓存问题
 const USER_CHANGE_EVENT = 'hubble:user-change';
+let logoutTransition = false;
 
 const notifyUserChange = () => {
     window.dispatchEvent(new CustomEvent(USER_CHANGE_EVENT));
@@ -56,6 +57,16 @@ const clearLogin = () => {
     clearUser();
 };
 
+const beginLogoutTransition = () => {
+    logoutTransition = true;
+};
+
+const endLogoutTransition = () => {
+    logoutTransition = false;
+};
+
+const isLogoutTransition = () => logoutTransition;
+
 const getDefaultGraphspace = () => {
     const user = getUser();
 
@@ -80,6 +91,9 @@ export {
     scopedStorageKey,
     clearUser,
     clearLogin,
+    beginLogoutTransition,
+    endLogoutTransition,
+    isLogoutTransition,
     getDefaultGraphspace,
     USER_CHANGE_EVENT,
 };