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 018c9b6ae4e70012c6e82182b9ed9b2ab972b51d
Author: dark <[email protected]>
AuthorDate: Sun Aug 30 14:23:35 2026 +0800

    fix(hubble): grant standalone account access
    
    - assign one shared read-write role to standalone accounts
    - keep legacy account labels aligned with actual membership
    - hide GraphSpace-only resource details in standalone mode
    - cover rollback, batch creation, and account rendering
---
 .../auth/StandaloneAccountPermissionService.java   | 203 +++++++++++++++++++++
 .../apache/hugegraph/service/auth/UserService.java |  34 ++++
 .../StandaloneAccountPermissionServiceTest.java    | 158 ++++++++++++++++
 .../org/apache/hugegraph/unit/UnitTestSuite.java   |   2 +
 .../unit/UserServiceCompatibilityTest.java         |  36 +++-
 .../hubble-fe/src/pages/Account/EditLayer.js       |  28 ++-
 .../src/pages/Account/account-recovery.test.js     |  36 ++++
 .../hubble-fe/src/pages/Account/index.js           |  12 +-
 8 files changed, 495 insertions(+), 14 deletions(-)

diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/StandaloneAccountPermissionService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/StandaloneAccountPermissionService.java
new file mode 100644
index 000000000..e665885be
--- /dev/null
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/StandaloneAccountPermissionService.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hugegraph.service.auth;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.hugegraph.driver.AuthManager;
+import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.structure.auth.Access;
+import org.apache.hugegraph.structure.auth.Belong;
+import org.apache.hugegraph.structure.auth.Group;
+import org.apache.hugegraph.structure.auth.HugePermission;
+import org.apache.hugegraph.structure.auth.HugeResourceType;
+import org.apache.hugegraph.structure.auth.Target;
+import org.apache.hugegraph.structure.auth.User;
+import org.apache.hugegraph.util.E;
+import org.springframework.stereotype.Service;
+
+@Service
+public class StandaloneAccountPermissionService {
+
+    private static final String ALL_GRAPHS = "*";
+    private static final String ROLE_NAME = "hubble_standalone_read_write";
+    private static final List<HugePermission> PERMISSIONS = Arrays.asList(
+            HugePermission.READ, HugePermission.WRITE,
+            HugePermission.DELETE, HugePermission.EXECUTE);
+
+    public void assignReadWrite(HugeClient client, User user) {
+        E.checkNotNull(user, "User");
+        E.checkNotNull(user.id(), "User id");
+
+        AuthManager auth = client.auth();
+        List<Group> groups = auth.listGroups().stream()
+                                 .filter(group -> 
ROLE_NAME.equals(group.name()))
+                                 .collect(Collectors.toList());
+        List<Target> targets = auth.listTargets().stream()
+                                   .filter(target -> ROLE_NAME.equals(
+                                           target.name()))
+                                   .collect(Collectors.toList());
+        E.checkState(groups.size() <= 1 && targets.size() <= 1,
+                     "Conflicting standalone read-write role");
+        E.checkState(groups.isEmpty() == targets.isEmpty(),
+                     "Incomplete standalone read-write role");
+
+        if (!groups.isEmpty()) {
+            Group group = groups.get(0);
+            Target target = targets.get(0);
+            this.validateRole(auth, group, target);
+            this.ensureBelong(auth, user, group);
+            return;
+        }
+
+        Group group = null;
+        Target target = null;
+        List<Access> accesses = new ArrayList<>();
+        try {
+            group = this.createGroup(auth);
+            target = this.createTarget(auth);
+            for (HugePermission permission : PERMISSIONS) {
+                Access access = new Access();
+                access.group(group);
+                access.target(target);
+                access.permission(permission);
+                accesses.add(auth.createAccess(access));
+            }
+            this.ensureBelong(auth, user, group);
+        } catch (RuntimeException error) {
+            this.rollbackRole(auth, accesses, target, group, error);
+            throw error;
+        }
+    }
+
+    private Group createGroup(AuthManager auth) {
+        Group group = new Group();
+        group.name(ROLE_NAME);
+        return auth.createGroup(group);
+    }
+
+    public boolean hasReadWrite(HugeClient client, Object userId) {
+        return this.readWriteUsers(client).contains(userId.toString());
+    }
+
+    public Set<String> readWriteUsers(HugeClient client) {
+        AuthManager auth = client.auth();
+        List<Group> groups = auth.listGroups().stream()
+                                 .filter(group -> 
ROLE_NAME.equals(group.name()))
+                                 .collect(Collectors.toList());
+        List<Target> targets = auth.listTargets().stream()
+                                   .filter(target -> ROLE_NAME.equals(
+                                           target.name()))
+                                   .collect(Collectors.toList());
+        if (groups.size() != 1 || targets.size() != 1) {
+            return Collections.emptySet();
+        }
+        try {
+            this.validateRole(auth, groups.get(0), targets.get(0));
+        } catch (IllegalStateException ignored) {
+            return Collections.emptySet();
+        }
+        Object groupId = groups.get(0).id();
+        return auth.listBelongsByGroup(groupId, -1).stream()
+                   .map(belong -> belong.user().toString())
+                   .collect(Collectors.toSet());
+    }
+
+    private Target createTarget(AuthManager auth) {
+        Target target = new Target();
+        target.name(ROLE_NAME);
+        target.graph(ALL_GRAPHS);
+        Map<String, Object> resource = new HashMap<>();
+        resource.put("type", HugeResourceType.ALL.toString());
+        resource.put("label", "*");
+        resource.put("properties", null);
+        target.resources(Collections.singletonList(resource));
+        return auth.createTarget(target);
+    }
+
+    private void validateRole(AuthManager auth, Group group, Target target) {
+        E.checkState(group.id() != null && target.id() != null &&
+                     ALL_GRAPHS.equals(target.graph()),
+                     "Conflicting standalone read-write role");
+        List<Map<String, Object>> resources = target.resourcesList();
+        E.checkState(resources != null && resources.size() == 1,
+                     "Conflicting standalone read-write target");
+        Map<String, Object> resource = resources.get(0);
+        E.checkState(HugeResourceType.ALL.toString().equals(
+                             resource.get("type")) &&
+                     "*".equals(resource.get("label")) &&
+                     resource.get("properties") == null,
+                     "Conflicting standalone read-write target");
+
+        List<Access> accesses = auth.listAccessesByGroup(group.id(), -1);
+        Set<HugePermission> permissions = new HashSet<>();
+        for (Access access : accesses) {
+            E.checkState(target.id().equals(access.target()),
+                         "Conflicting standalone read-write access");
+            permissions.add(access.permission());
+        }
+        E.checkState(accesses.size() == PERMISSIONS.size() &&
+                     permissions.equals(new HashSet<>(PERMISSIONS)),
+                     "Conflicting standalone read-write access");
+    }
+
+    private void ensureBelong(AuthManager auth, User user, Group group) {
+        if (auth.listBelongsByUser(user.id(), -1).stream()
+                .anyMatch(belong -> group.id().equals(belong.group()))) {
+            return;
+        }
+        Belong belong = new Belong();
+        belong.user(user);
+        belong.group(group);
+        auth.createBelong(belong);
+    }
+
+    private void rollbackRole(AuthManager auth, List<Access> accesses,
+                              Target target, Group group,
+                              RuntimeException failure) {
+        for (Access access : accesses) {
+            if (access != null && access.id() != null) {
+                this.suppressRollback(() -> auth.deleteAccess(access.id()),
+                                      failure);
+            }
+        }
+        if (target != null && target.id() != null) {
+            this.suppressRollback(() -> auth.deleteTarget(target.id()),
+                                  failure);
+        }
+        if (group != null && group.id() != null) {
+            this.suppressRollback(() -> auth.deleteGroup(group.id()), failure);
+        }
+    }
+
+    private void suppressRollback(Runnable rollback, RuntimeException failure) 
{
+        try {
+            rollback.run();
+        } catch (RuntimeException error) {
+            failure.addSuppressed(error);
+        }
+    }
+}
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 d1c448439..fde0b8824 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
@@ -27,6 +27,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -71,6 +72,8 @@ public class UserService extends AuthService {
     private HugeConfig config;
     @Autowired
     private GraphSpaceUserService graphSpaceUserService;
+    @Autowired
+    private StandaloneAccountPermissionService standalonePermissions;
 
     private boolean isPdEnabled() {
         return config.get(HubbleOptions.PD_ENABLED);
@@ -83,12 +86,17 @@ public class UserService extends AuthService {
         List<UserEntity> ues = new ArrayList<>(users.size());
         Map<String, Integer> countMap = new HashMap<>();
         Map<String, List<String>> spaceMap = new HashMap<>();
+        Set<String> standaloneReadWrite = isPdEnabled() ?
+                                          new HashSet<>() :
+                                          this.standalonePermissions
+                                              .readWriteUsers(hugeClient);
         users.forEach(u -> {
             UserEntity ue = convert(hugeClient, u);
             if (isPdEnabled()) {
                 ue.setSuperadmin(isSuperAdmin(hugeClient, ue.getId()));
             } else {
                 ue.setSuperadmin(isStandaloneAdmin(ue.getName()));
+                this.populateStandalonePermission(ue, standaloneReadWrite);
             }
             ues.add(ue);
         });
@@ -139,8 +147,12 @@ public class UserService extends AuthService {
             this.populatePermissionPresets(hugeClient, page.getRecords());
             return page;
         } else {
+            Set<String> standaloneReadWrite =
+                    this.standalonePermissions.readWriteUsers(hugeClient);
             for (UserEntity user : results) {
                 user.setSuperadmin(isStandaloneAdmin(user.getName()));
+                this.populateStandalonePermission(user,
+                                                  standaloneReadWrite);
             }
         }
         return PageUtil.page(results, pageNo, pageSize);
@@ -181,6 +193,7 @@ public class UserService extends AuthService {
             userEntity.setAdminSpaces(new ArrayList<>());
             userEntity.setSpacenum(0);
             userEntity.setResSpaces(new ArrayList<>());
+            this.populateStandalonePermission(hugeClient, userEntity);
         }
         return userEntity;
     }
@@ -249,6 +262,9 @@ public class UserService extends AuthService {
                 superAdminAttempted = true;
                 client.auth().addSuperAdmin(ue.getName());
             }
+            if (!isPdEnabled()) {
+                this.standalonePermissions.assignReadWrite(client, newUser);
+            }
         } catch (RuntimeException error) {
             this.rollbackNewAccount(client, newUser, ue.getName(),
                                     superAdminAttempted, error);
@@ -279,6 +295,21 @@ public class UserService extends AuthService {
         }
     }
 
+    private void populateStandalonePermission(HugeClient client,
+                                              UserEntity user) {
+        if (!user.isSuperadmin() &&
+            this.standalonePermissions.hasReadWrite(client, user.getId())) {
+            user.setPermissionPreset("GS_READ_WRITE");
+        }
+    }
+
+    private void populateStandalonePermission(UserEntity user,
+                                              Set<String> readWriteUsers) {
+        if (!user.isSuperadmin() && readWriteUsers.contains(user.getId())) {
+            user.setPermissionPreset("GS_READ_WRITE");
+        }
+    }
+
     public String addbatch(HugeClient client, MultipartFile csvFile) {
         File file = multipartFileToFile(csvFile);
         try {
@@ -292,6 +323,9 @@ public class UserService extends AuthService {
             for (Map<String, String> entry : resultList) {
                 if (!CREATE_SUCCESS.equals(entry.get("result"))) {
                     failedList.add(entry.get("user_name"));
+                } else if (!isPdEnabled()) {
+                    User user = client.findUserByName(entry.get("user_name"));
+                    this.standalonePermissions.assignReadWrite(client, user);
                 }
             }
             if (!failedList.isEmpty()) {
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/StandaloneAccountPermissionServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/StandaloneAccountPermissionServiceTest.java
new file mode 100644
index 000000000..f24659abf
--- /dev/null
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/StandaloneAccountPermissionServiceTest.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hugegraph.service.auth;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hugegraph.driver.AuthManager;
+import org.apache.hugegraph.driver.GraphsManager;
+import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.structure.auth.Access;
+import org.apache.hugegraph.structure.auth.Belong;
+import org.apache.hugegraph.structure.auth.Group;
+import org.apache.hugegraph.structure.auth.HugePermission;
+import org.apache.hugegraph.structure.auth.Target;
+import org.apache.hugegraph.structure.auth.User;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+public class StandaloneAccountPermissionServiceTest {
+
+    @Test
+    public void testAssignReadWriteUsesOneSharedRole() {
+        HugeClient client = Mockito.mock(HugeClient.class);
+        AuthManager auth = Mockito.mock(AuthManager.class);
+        GraphsManager graphs = Mockito.mock(GraphsManager.class);
+        Group group = group();
+        Target target = target();
+        User user = new User();
+        user.setId("user-id");
+
+        Mockito.when(client.auth()).thenReturn(auth);
+        Mockito.when(client.graphs()).thenReturn(graphs);
+        Mockito.when(auth.listGroups())
+               .thenReturn(Collections.singletonList(group));
+        Mockito.when(auth.listTargets())
+               .thenReturn(Collections.singletonList(target));
+        Mockito.when(graphs.listGraph())
+               .thenReturn(Collections.singletonList("hugegraph"));
+        Mockito.when(auth.listAccessesByGroup("group-id", -1))
+               .thenReturn(accesses(group, target));
+        Mockito.when(auth.listBelongsByUser("user-id", -1))
+               .thenReturn(Collections.emptyList());
+
+        new StandaloneAccountPermissionService()
+                .assignReadWrite(client, user);
+
+        Mockito.verify(auth, Mockito.never())
+               .createAccess(Mockito.any(Access.class));
+        ArgumentCaptor<Belong> belong =
+                ArgumentCaptor.forClass(Belong.class);
+        Mockito.verify(auth).createBelong(belong.capture());
+        Assert.assertEquals("user-id", belong.getValue().user());
+        Assert.assertEquals("group-id", belong.getValue().group());
+    }
+
+    @Test
+    public void testGrantFailureRollsBackNewRole() {
+        HugeClient client = Mockito.mock(HugeClient.class);
+        AuthManager auth = Mockito.mock(AuthManager.class);
+        GraphsManager graphs = Mockito.mock(GraphsManager.class);
+        Group group = group();
+        Target target = target();
+        User user = new User();
+        user.setId("user-id");
+
+        Mockito.when(client.auth()).thenReturn(auth);
+        Mockito.when(client.graphs()).thenReturn(graphs);
+        Mockito.when(graphs.listGraph())
+               .thenReturn(Collections.singletonList("hugegraph"));
+        Mockito.when(auth.listGroups()).thenReturn(Collections.emptyList());
+        Mockito.when(auth.listTargets()).thenReturn(Collections.emptyList());
+        Mockito.when(auth.createGroup(Mockito.any(Group.class)))
+               .thenReturn(group);
+        Mockito.when(auth.createTarget(Mockito.any(Target.class)))
+               .thenReturn(target);
+        Mockito.when(auth.createAccess(Mockito.any(Access.class)))
+               .thenAnswer(invocation -> {
+                   Access access = invocation.getArgument(0);
+                   access.setId("access-" + access.permission());
+                   return access;
+               });
+        Mockito.when(auth.listBelongsByUser("user-id", -1))
+               .thenReturn(Collections.emptyList());
+        RuntimeException failure = new RuntimeException("grant failed");
+        Mockito.when(auth.createBelong(Mockito.any(Belong.class)))
+               .thenThrow(failure);
+
+        try {
+            new StandaloneAccountPermissionService()
+                    .assignReadWrite(client, user);
+            Assert.fail("Expected the standalone grant to fail");
+        } catch (RuntimeException error) {
+            Assert.assertSame(failure, error);
+        }
+
+        Mockito.verify(auth, Mockito.times(4))
+               .deleteAccess(Mockito.any());
+        Mockito.verify(auth).deleteTarget("target-id");
+        Mockito.verify(auth).deleteGroup("group-id");
+    }
+
+    private static Group group() {
+        Group group = new Group();
+        group.setId("group-id");
+        group.name("hubble_standalone_read_write");
+        return group;
+    }
+
+    private static Target target() {
+        Target target = new Target();
+        target.setId("target-id");
+        target.name("hubble_standalone_read_write");
+        target.graph("*");
+        Map<String, Object> resource = new HashMap<>();
+        resource.put("type", "ALL");
+        resource.put("label", "*");
+        resource.put("properties", null);
+        target.resources(Collections.singletonList(resource));
+        return target;
+    }
+
+    private static List<Access> accesses(Group group, Target target) {
+        return Arrays.stream(HugePermission.values())
+                     .filter(permission -> permission == HugePermission.READ ||
+                                           permission == HugePermission.WRITE 
||
+                                           permission == HugePermission.DELETE 
||
+                                           permission == 
HugePermission.EXECUTE)
+                     .map(permission -> {
+                         Access access = new Access();
+                         access.group(group);
+                         access.target(target);
+                         access.permission(permission);
+                         return access;
+                     })
+                     .collect(java.util.stream.Collectors.toList());
+    }
+}
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java
index c7ed4f658..14c8b8214 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -32,6 +32,7 @@ import 
org.apache.hugegraph.service.load.IngestTransactionIntegrationTest;
 import org.apache.hugegraph.service.auth.AuthContextServiceTest;
 import org.apache.hugegraph.service.auth.AuthModeServiceTest;
 import org.apache.hugegraph.service.auth.GraphSpaceUserServiceTest;
+import 
org.apache.hugegraph.service.auth.StandaloneAccountPermissionServiceTest;
 import org.apache.hugegraph.service.space.GraphSpaceServiceTest;
 import org.apache.hugegraph.service.op.DefaultOperationsDataServiceTest;
 import org.apache.hugegraph.service.op.LiveOperationsCollectorTest;
@@ -66,6 +67,7 @@ import org.junit.runners.Suite;
     GraphSpaceAuthOwnershipTest.class,
     GraphSpaceServiceTest.class,
     GraphSpaceUserServiceTest.class,
+    StandaloneAccountPermissionServiceTest.class,
     GraphsControllerCanonicalTest.class,
     GremlinUtilTest.class,
     GremlinHistoryFailureTest.class,
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 aa7b78756..d7ac899c7 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
@@ -42,6 +42,7 @@ 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.StandaloneAccountPermissionService;
 import org.apache.hugegraph.service.auth.UserService;
 import org.apache.hugegraph.structure.auth.User;
 
@@ -53,6 +54,7 @@ public class UserServiceCompatibilityTest {
     private GraphSpaceManager graphSpace;
     private GraphsManager graphs;
     private GraphSpaceUserService graphSpaceUsers;
+    private StandaloneAccountPermissionService standalonePermissions;
     private UserService service;
 
     @Before
@@ -63,6 +65,8 @@ public class UserServiceCompatibilityTest {
         this.graphSpace = Mockito.mock(GraphSpaceManager.class);
         this.graphs = Mockito.mock(GraphsManager.class);
         this.graphSpaceUsers = Mockito.mock(GraphSpaceUserService.class);
+        this.standalonePermissions =
+                Mockito.mock(StandaloneAccountPermissionService.class);
         Mockito.when(this.client.auth()).thenReturn(this.auth);
         Mockito.when(this.client.graphSpace()).thenReturn(this.graphSpace);
         Mockito.when(this.client.graphs()).thenReturn(this.graphs);
@@ -74,12 +78,13 @@ public class UserServiceCompatibilityTest {
         ReflectionTestUtils.setField(this.service, "config", this.config);
         ReflectionTestUtils.setField(this.service, "graphSpaceUserService",
                                      this.graphSpaceUsers);
+        ReflectionTestUtils.setField(this.service, "standalonePermissions",
+                                     this.standalonePermissions);
     }
 
     @Test
     public void testStandaloneUserCreationOmitsPdOnlyNickname() {
         
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(false);
-
         UserEntity created =
                 this.service.add(this.client, userEntity("display-name"));
 
@@ -87,6 +92,8 @@ public class UserServiceCompatibilityTest {
         Mockito.verify(this.auth).createUser(request.capture());
         Assert.assertNull(request.getValue().nickname());
         Assert.assertEquals("created-id", created.getId());
+        Mockito.verify(this.standalonePermissions).assignReadWrite(
+                Mockito.eq(this.client), Mockito.any(User.class));
     }
 
     @Test
@@ -98,6 +105,29 @@ public class UserServiceCompatibilityTest {
         ArgumentCaptor<User> request = ArgumentCaptor.forClass(User.class);
         Mockito.verify(this.auth).createUser(request.capture());
         Assert.assertEquals("display-name", request.getValue().nickname());
+        Mockito.verifyZeroInteractions(this.standalonePermissions);
+    }
+
+    @Test
+    public void testStandaloneGrantFailureDeletesNewAccount() {
+        
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(false);
+        User created = user("created-user");
+        Mockito.when(this.auth.createUser(Mockito.any(User.class)))
+               .thenReturn(created);
+        RuntimeException failure = new RuntimeException("grant failed");
+        Mockito.doThrow(failure).when(this.standalonePermissions)
+               .assignReadWrite(this.client, created);
+
+        RuntimeException error = null;
+        try {
+            this.service.add(this.client, userEntity("display-name"));
+        } catch (RuntimeException e) {
+            error = e;
+        }
+
+        Assert.assertNotNull(error);
+        Assert.assertSame(failure, error);
+        Mockito.verify(this.auth).deleteUser("created-user");
     }
 
     @Test
@@ -105,6 +135,8 @@ public class UserServiceCompatibilityTest {
         
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(false);
         Mockito.when(this.auth.listUsers())
                .thenReturn(Arrays.asList(user("admin"), user("hubbleuser")));
+        Mockito.when(this.standalonePermissions.readWriteUsers(this.client))
+               .thenReturn(Collections.singleton("hubbleuser"));
 
         @SuppressWarnings("unchecked")
         IPage<UserEntity> result = (IPage<UserEntity>)
@@ -112,6 +144,8 @@ public class UserServiceCompatibilityTest {
 
         Assert.assertTrue(result.getRecords().get(0).isSuperadmin());
         Assert.assertFalse(result.getRecords().get(1).isSuperadmin());
+        Assert.assertEquals("GS_READ_WRITE",
+                            result.getRecords().get(1).getPermissionPreset());
     }
 
     @Test
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index 9fe7213e7..44ec0d3bc 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -67,6 +67,7 @@ const EditLayer = ({
     const detailRequest = useRef(0);
     const permissionPresetsSupported = !context
         || context.capabilities?.includes('account_permission_presets');
+    const standalone = context?.mode === 'NON_PD';
 
     const title = {
         'detail': t('account.form.title_detail'),
@@ -252,19 +253,28 @@ const EditLayer = ({
                                 label={t('account.form.permission_preset')}
                                 className={style.item}
                             >
-                                
{t(`account.permission_preset.${getAccountPresetLabelKey(
-                                    detail, permissionPresetsSupported
-                                )}`)}
+                                {t(`account.permission_preset.${
+                                    standalone
+                                    && detail.permission_preset
+                                    === PERMISSION_PRESETS.GS_READ_WRITE
+                                        ? PERMISSION_PRESETS.GS_READ_WRITE
+                                        : getAccountPresetLabelKey(
+                                            detail,
+                                            permissionPresetsSupported
+                                        )
+                                }`)}
                             </Form.Item>
                             <Form.Item label={t('account.form.remark')} 
className={style.item}>
                                 {detail.user_description}
                             </Form.Item>
-                            <Form.Item
-                                label={t('account.form.graphspaces')}
-                                className={style.item}
-                            >
-                                {getPresetSpaces(detail).join(', ')}
-                            </Form.Item>
+                            {!standalone && (
+                                <Form.Item
+                                    label={t('account.form.graphspaces')}
+                                    className={style.item}
+                                >
+                                    {getPresetSpaces(detail).join(', ')}
+                                </Form.Item>
+                            )}
                             <Form.Item label={t('account.col.create_time')} 
className={style.item}>
                                 {detail.user_create}
                             </Form.Item>
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
index dc32f96d2..e79dc7b19 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
@@ -173,6 +173,42 @@ test('does not label an unassigned legacy account as 
GraphSpace read-only', asyn
         .not.toBeInTheDocument();
 });
 
+test('shows standalone accounts as read-write without GraphSpace resources',
+    async () => {
+        mockAuthContext.context = {
+            mode: 'NON_PD',
+            capabilities: [],
+            actions: {
+                accounts: ['create', 'read', 'update', 'delete'],
+                authorizations: [],
+            },
+        };
+        api.auth.getAllUserList.mockResolvedValue({
+            status: 200,
+            data: {
+                records: [{
+                    user_name: 'writer',
+                    permission_preset: 'GS_READ_WRITE',
+                }, {
+                    user_name: 'legacy',
+                }],
+                total: 2,
+            },
+        });
+
+        render(<Account />);
+
+        expect(await screen.findByText(
+            'account.permission_preset.GS_READ_WRITE'
+        )).toBeInTheDocument();
+        expect(screen.getByText('account.permission_preset.unassigned'))
+            .toBeInTheDocument();
+        expect(screen.queryByText('account.col.resource'))
+            .not.toBeInTheDocument();
+        expect(screen.queryByText('account.space_access.scoped_tab'))
+            .not.toBeInTheDocument();
+    });
+
 test('space administrators use scoped management without loading global 
accounts', async () => {
     mockCurrentUser = {
         id: 'space-admin',
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
index 59b037882..9ada4a501 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
@@ -68,6 +68,7 @@ const GlobalAccounts = ({
     const canGrantAuthorization = authorizationActions.includes('grant');
     const permissionPresetsSupported = !context
         || context.capabilities?.includes('account_permission_presets');
+    const standalone = context?.mode === 'NON_PD';
     const hasRowMutations = canUpdateAccount || canDeleteAccount || 
canGrantAuthorization;
     const [editLayerVisible, setEditLayerVisible] = useState(false);
     const [creationContext, setCreationContext] = useState(null);
@@ -201,9 +202,11 @@ const GlobalAccounts = ({
                 const preset = getAccountPreset(row);
                 const color = preset === PERMISSION_PRESETS.SUPER_ADMIN ? 'red'
                     : preset === PERMISSION_PRESETS.GS_ADMIN ? 'blue' : 
'default';
-                const labelKey = getAccountPresetLabelKey(
-                    row, permissionPresetsSupported
-                );
+                const labelKey = standalone
+                                 && row.permission_preset
+                                 === PERMISSION_PRESETS.GS_READ_WRITE
+                    ? PERMISSION_PRESETS.GS_READ_WRITE
+                    : getAccountPresetLabelKey(row, 
permissionPresetsSupported);
                 return (
                     <Tag color={color}>
                         {t(`account.permission_preset.${labelKey}`)}
@@ -212,6 +215,7 @@ const GlobalAccounts = ({
             },
         },
         {
+            key: 'graphspaces',
             title: t('account.col.resource'),
             width: 120,
             render: row => (
@@ -285,7 +289,7 @@ const GlobalAccounts = ({
                 );
             },
         },
-    ];
+    ].filter(column => !standalone || column.key !== 'graphspaces');
 
     const rowKey = useCallback(item => item.user_name, []);
     const {current, pageSize} = pagination;

Reply via email to