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;
