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:17 UTC
(hugegraph-toolchain) 02/24: fix(hubble): harden compatibility contracts
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 554f92b6b16a4bc4c880923c361daec63301c2a9
Author: dark <ji...@apache.org>
AuthorDate: Sun Aug 16 16:40:07 2026 +0800
fix(hubble): harden compatibility contracts
- preserve member-manager compatibility
- align version and CI capability boundaries
- normalize PR-added source formatting
---
AGENTS.md | 2 +-
README.md | 6 +-
.../org/apache/hugegraph/driver/HugeClient.java | 22 +-
.../hugegraph/driver/ServerCompatibility.java | 24 ++-
.../hugegraph/driver/ServerCompatibilityTest.java | 4 +-
hugegraph-hubble/README.md | 15 +-
.../docs/images/compat-1.5-standalone.png | Bin 0 -> 82286 bytes
.../docs/images/compat-1.7-standalone.png | Bin 0 -> 81494 bytes
.../docs/images/compat-1.8-auth-account.png | Bin 0 -> 68710 bytes
.../docs/images/compat-1.8-auth-dashboard.png | Bin 0 -> 106470 bytes
.../docs/images/compat-1.8-graphspace-access.png | Bin 0 -> 62654 bytes
hugegraph-hubble/docs/images/compat-1.8-pd.png | Bin 0 -> 28788 bytes
.../docs/images/hubble-auth-connection-root.png | Bin 56400 -> 0 bytes
.../docs/images/hubble-auth-connection.png | Bin 41540 -> 0 bytes
.../docs/images/hubble-graphspace-permissions.png | Bin 60284 -> 0 bytes
.../docs/images/hubble-legacy-capability.png | Bin 45865 -> 0 bytes
hugegraph-hubble/docs/images/hubble-non-auth.png | Bin 41540 -> 0 bytes
.../docs/images/hubble-pd-connection.png | Bin 47441 -> 0 bytes
.../hugegraph/controller/BaseController.java | 17 +-
.../controller/auth/AccessController.java | 10 +-
.../controller/auth/BelongController.java | 16 +-
.../controller/auth/GraphSpaceUserController.java | 11 +
.../hugegraph/controller/auth/LoginController.java | 5 +-
.../hugegraph/controller/auth/RoleController.java | 12 +-
.../controller/auth/TargetController.java | 12 +-
.../controller/space/GraphSpaceController.java | 12 ++
.../hugegraph/handler/CustomInterceptor.java | 56 +++--
.../apache/hugegraph/handler/LoginInterceptor.java | 16 ++
.../apache/hugegraph/options/HubbleOptions.java | 6 +-
.../hugegraph/service/HugeClientPoolService.java | 4 +
.../hugegraph/service/auth/AuthContextService.java | 15 +-
.../hugegraph/service/auth/AuthModeService.java | 2 +-
.../service/auth/GraphSpaceUserService.java | 236 +++++++++++++++------
.../apache/hugegraph/service/auth/UserService.java | 161 +++++++++-----
.../hugegraph/service/space/GraphSpaceService.java | 44 +++-
.../GraphSpaceAuthMutationAuthorizationTest.java | 52 ++++-
.../auth/GraphSpaceAuthOwnershipTest.java | 41 +++-
.../service/auth/AuthContextServiceTest.java | 6 +-
.../apache/hugegraph/unit/AuthSecurityTest.java | 60 ++++++
.../unit/UserServiceCompatibilityTest.java | 4 +
hugegraph-hubble/hubble-fe/src/App.test.js | 36 +++-
.../hubble-fe/src/api/auth-contract.test.js | 18 ++
hugegraph-hubble/hubble-fe/src/api/auth.js | 25 +++
.../hubble-fe/src/components/Sidebar/index.ant.js | 6 +-
.../src/components/Sidebar/index.ant.test.js | 21 ++
.../src/i18n/resources/en-US/modules/pages.json | 4 +-
.../src/i18n/resources/zh-CN/modules/pages.json | 4 +-
.../hubble-fe/src/pages/Account/EditLayer.js | 22 +-
.../hubble-fe/src/pages/Account/SpaceAccess.js | 84 +++++---
.../src/pages/Account/SpaceAccess.test.js | 62 ++++--
.../hubble-fe/src/pages/Account/index.js | 3 +
.../src/pages/Account/permissionPresets.js | 32 ++-
.../src/pages/Account/permissionPresets.test.js | 19 ++
hugegraph-hubble/hubble-fe/src/routes/index.js | 18 +-
.../hubble-fe/src/routes/route-guard.test.js | 21 +-
tools/checkstyle.xml | 2 +-
56 files changed, 964 insertions(+), 284 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 4aa6cceaa..9f8ca9eb7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -79,7 +79,7 @@ mvn test -Dtest=FuncTestSuite -pl hugegraph-tools -ntp
## Code Style
Checkstyle enforced via `tools/checkstyle.xml`:
-- Max line length: 100 characters
+- Max line length: 120 characters
- 4-space indentation (no tabs)
- No star imports
- No `System.out.println`
diff --git a/README.md b/README.md
index 3eb7a6253..bff20dad7 100644
--- a/README.md
+++ b/README.md
@@ -19,9 +19,11 @@ four readable presets. A thin adapter keeps 1.7 usable and limits 1.5 to its
standalone core graph workflow; version checks are centralized rather than
spread across UI pages.
-
+
-
+
+
+
**Quick Navigation**: [Architecture](#architecture-overview) | [Quick Start](#quick-start) | [Modules](#module-overview) | [Build](#build--development) | [Docker](#docker) | [Related Projects](#related-projects)
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 f02fd6146..578b9ce4b 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
@@ -24,6 +24,7 @@ import lombok.Getter;
import org.apache.hugegraph.client.RestClient;
import org.apache.hugegraph.rest.ClientException;
import org.apache.hugegraph.rest.RestClientConfig;
+import org.apache.hugegraph.structure.auth.User;
import org.apache.hugegraph.util.VersionUtil;
import org.apache.hugegraph.version.ClientVersion;
import org.slf4j.Logger;
@@ -57,6 +58,8 @@ public class HugeClient implements Closeable {
*/
private volatile boolean apiVersionChecked;
private final Object apiVersionLock = new Object();
+ private ServerCompatibility.Profile compatibility =
+ ServerCompatibility.Profile.LEGACY;
private VersionManager version;
private GraphsManager graphs;
private SchemaManager schema;
@@ -209,9 +212,8 @@ public class HugeClient implements Closeable {
// 0.81 equals to the {latest_api_version} +10
VersionUtil.check(apiVersion, "0.38", "0.81", "hugegraph-api in server");
this.client.apiVersion(apiVersion);
- boolean supportGs = ServerCompatibility.supportsGraphSpace(
- this.version.getCoreVersion());
- this.client.setSupportGs(supportGs);
+ this.compatibility = ServerCompatibility.profile(this.version.getCoreVersion());
+ this.client.setSupportGs(this.compatibility.supportsGraphSpace());
}
public GraphsManager graphs() {
@@ -258,6 +260,20 @@ public class HugeClient implements Closeable {
return this.auth;
}
+ public boolean supportsDefaultRole() {
+ return this.compatibility.supportsDefaultRole();
+ }
+
+ public User findUserByName(String name) {
+ if (this.supportsDefaultRole()) {
+ return this.auth.getUserByName(name);
+ }
+ return this.auth.listUsers().stream()
+ .filter(user -> name.equals(user.name()))
+ .findFirst()
+ .orElse(null);
+ }
+
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 ac975d661..18fb6bd2c 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
@@ -29,6 +29,7 @@ import org.apache.hugegraph.util.VersionUtil;
public final class ServerCompatibility {
private static final String GRAPHSPACE_MIN_VERSION = "1.7.0";
+ private static final String DEFAULT_ROLE_MIN_VERSION = "1.8.0";
private ServerCompatibility() {
}
@@ -39,8 +40,10 @@ public final class ServerCompatibility {
}
try {
String normalized = coreVersion.trim();
- return VersionUtil.gte(normalized, GRAPHSPACE_MIN_VERSION) ?
- Profile.MODERN : Profile.LEGACY;
+ if (VersionUtil.gte(normalized, DEFAULT_ROLE_MIN_VERSION)) {
+ return Profile.MODERN;
+ }
+ return VersionUtil.gte(normalized, GRAPHSPACE_MIN_VERSION) ? Profile.GRAPHSPACE : Profile.LEGACY;
} catch (RuntimeException ignored) {
return Profile.LEGACY;
}
@@ -50,18 +53,29 @@ public final class ServerCompatibility {
return profile(coreVersion).supportsGraphSpace();
}
+ public static boolean supportsDefaultRole(String coreVersion) {
+ return profile(coreVersion).supportsDefaultRole();
+ }
+
public enum Profile {
- LEGACY(false),
- MODERN(true);
+ LEGACY(false, false),
+ GRAPHSPACE(true, false),
+ MODERN(true, true);
private final boolean graphSpace;
+ private final boolean defaultRole;
- Profile(boolean graphSpace) {
+ Profile(boolean graphSpace, boolean defaultRole) {
this.graphSpace = graphSpace;
+ this.defaultRole = defaultRole;
}
public boolean supportsGraphSpace() {
return this.graphSpace;
}
+
+ public boolean supportsDefaultRole() {
+ return this.defaultRole;
+ }
}
}
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 f3bccbf17..96f98ad0c 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
@@ -35,7 +35,9 @@ public class ServerCompatibilityTest {
Assert.assertTrue(ServerCompatibility.supportsGraphSpace("1.7.0"));
Assert.assertTrue(ServerCompatibility.supportsGraphSpace(" 1.7.0 "));
Assert.assertTrue(ServerCompatibility.supportsGraphSpace("1.8.0"));
- Assert.assertEquals(ServerCompatibility.Profile.MODERN,
+ Assert.assertFalse(ServerCompatibility.supportsDefaultRole("1.7.0"));
+ Assert.assertTrue(ServerCompatibility.supportsDefaultRole("1.8.0"));
+ Assert.assertEquals(ServerCompatibility.Profile.GRAPHSPACE,
ServerCompatibility.profile("1.7.1"));
}
}
diff --git a/hugegraph-hubble/README.md b/hugegraph-hubble/README.md
index 5b3fb9955..4062f4f9f 100644
--- a/hugegraph-hubble/README.md
+++ b/hugegraph-hubble/README.md
@@ -34,15 +34,20 @@ being scattered through controllers or pages. See
The following captures are collected from Chrome against the running Hubble
application and document the supported states:
-
+
-
+
-
+
-
+
-
+
+
+
+
+The 1.5 and 1.7 captures show standalone non-auth core workflows. The 1.8/master
+captures cover both PD/non-auth connection discovery and authenticated account / GraphSpace permission surfaces.
## Local development feedback loop
diff --git a/hugegraph-hubble/docs/images/compat-1.5-standalone.png b/hugegraph-hubble/docs/images/compat-1.5-standalone.png
new file mode 100644
index 000000000..34223b135
Binary files /dev/null and b/hugegraph-hubble/docs/images/compat-1.5-standalone.png differ
diff --git a/hugegraph-hubble/docs/images/compat-1.7-standalone.png b/hugegraph-hubble/docs/images/compat-1.7-standalone.png
new file mode 100644
index 000000000..c656488e2
Binary files /dev/null and b/hugegraph-hubble/docs/images/compat-1.7-standalone.png differ
diff --git a/hugegraph-hubble/docs/images/compat-1.8-auth-account.png b/hugegraph-hubble/docs/images/compat-1.8-auth-account.png
new file mode 100644
index 000000000..e86c1c2d7
Binary files /dev/null and b/hugegraph-hubble/docs/images/compat-1.8-auth-account.png differ
diff --git a/hugegraph-hubble/docs/images/compat-1.8-auth-dashboard.png b/hugegraph-hubble/docs/images/compat-1.8-auth-dashboard.png
new file mode 100644
index 000000000..9bbffb315
Binary files /dev/null and b/hugegraph-hubble/docs/images/compat-1.8-auth-dashboard.png differ
diff --git a/hugegraph-hubble/docs/images/compat-1.8-graphspace-access.png b/hugegraph-hubble/docs/images/compat-1.8-graphspace-access.png
new file mode 100644
index 000000000..537694b0d
Binary files /dev/null and b/hugegraph-hubble/docs/images/compat-1.8-graphspace-access.png differ
diff --git a/hugegraph-hubble/docs/images/compat-1.8-pd.png b/hugegraph-hubble/docs/images/compat-1.8-pd.png
new file mode 100644
index 000000000..a72899a35
Binary files /dev/null and b/hugegraph-hubble/docs/images/compat-1.8-pd.png differ
diff --git a/hugegraph-hubble/docs/images/hubble-auth-connection-root.png b/hugegraph-hubble/docs/images/hubble-auth-connection-root.png
deleted file mode 100644
index 966624185..000000000
Binary files a/hugegraph-hubble/docs/images/hubble-auth-connection-root.png and /dev/null differ
diff --git a/hugegraph-hubble/docs/images/hubble-auth-connection.png b/hugegraph-hubble/docs/images/hubble-auth-connection.png
deleted file mode 100644
index f205e3621..000000000
Binary files a/hugegraph-hubble/docs/images/hubble-auth-connection.png and /dev/null differ
diff --git a/hugegraph-hubble/docs/images/hubble-graphspace-permissions.png b/hugegraph-hubble/docs/images/hubble-graphspace-permissions.png
deleted file mode 100644
index 06f4a0c3f..000000000
Binary files a/hugegraph-hubble/docs/images/hubble-graphspace-permissions.png and /dev/null differ
diff --git a/hugegraph-hubble/docs/images/hubble-legacy-capability.png b/hugegraph-hubble/docs/images/hubble-legacy-capability.png
deleted file mode 100644
index 982b789dc..000000000
Binary files a/hugegraph-hubble/docs/images/hubble-legacy-capability.png and /dev/null differ
diff --git a/hugegraph-hubble/docs/images/hubble-non-auth.png b/hugegraph-hubble/docs/images/hubble-non-auth.png
deleted file mode 100644
index f205e3621..000000000
Binary files a/hugegraph-hubble/docs/images/hubble-non-auth.png and /dev/null differ
diff --git a/hugegraph-hubble/docs/images/hubble-pd-connection.png b/hugegraph-hubble/docs/images/hubble-pd-connection.png
deleted file mode 100644
index 8decb8bf6..000000000
Binary files a/hugegraph-hubble/docs/images/hubble-pd-connection.png and /dev/null differ
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
index db091fe14..faf93d6ba 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
@@ -146,10 +146,9 @@ public abstract class BaseController {
client.assignGraph(graphSpace, graph);
return client;
}
- HugeClient client = this.authMode.anonymous() ?
- this.hugeClientPoolService.createUnauthClient() :
- this.hugeClientPoolService.createAuthClient(
- graphSpace, graph, this.getToken());
+ HugeClient client = this.authMode != null && this.authMode.anonymous() ?
+ this.hugeClientPoolService.createUnauthClient(graphSpace, graph) :
+ this.hugeClientPoolService.createAuthClient(graphSpace, graph, this.getToken());
if (graphSpace != null || graph != null) {
client.assignGraph(graphSpace, graph);
}
@@ -177,6 +176,16 @@ public abstract class BaseController {
return client;
}
+ protected HugeClient requireGraphSpaceAuthorizationAdmin(
+ String graphSpace) {
+ HugeClient client = this.authClient(null, null);
+ if (!this.userService.isSuperAdmin(client)) {
+ throw new ForbiddenException("Permission denied: manage authorization objects");
+ }
+ client.assignGraph(graphSpace, null);
+ return client;
+ }
+
protected HugeClient requireGraphSpaceAdministrator() {
HugeClient client = this.authClient(null, null);
if (!this.userService.isSuperAdmin(client)) {
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AccessController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AccessController.java
index 0f97026ed..f2bbc78f5 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AccessController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/AccessController.java
@@ -47,28 +47,28 @@ public class AccessController extends AuthController {
@PathVariable("graphspace") String graphSpace,
@RequestParam(value = "role_id", required = false) String roleId,
@RequestParam(value = "target_id", required = false) String targetId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.accessService.list(client, graphSpace, roleId, targetId);
}
@GetMapping("{id}")
public AccessEntity get(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String accessId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.accessService.get(client, graphSpace, accessId);
}
@PostMapping
public AccessEntity add(@PathVariable("graphspace") String graphSpace,
@RequestBody AccessEntity accessEntity) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.accessService.addOrUpdate(client, graphSpace, accessEntity);
}
@PutMapping
public AccessEntity update(@PathVariable("graphspace") String graphSpace,
@RequestBody AccessEntity accessEntity) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.accessService.addOrUpdate(client, graphSpace, accessEntity);
}
@@ -76,7 +76,7 @@ public class AccessController extends AuthController {
public void delete(@PathVariable("graphspace") String graphSpace,
@RequestParam("role_id") String roleId,
@RequestParam("target_id") String targetId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
this.accessService.delete(client, graphSpace, roleId, targetId);
}
}
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/BelongController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/BelongController.java
index 757a4152b..1f7ce5660 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/BelongController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/BelongController.java
@@ -48,7 +48,7 @@ public class BelongController extends AuthController {
@PathVariable("graphspace") String graphSpace,
@RequestParam(value = "role_id", required = false) String roleId,
@RequestParam(value = "user_id", required = false) String userId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.belongService.list(client, graphSpace, roleId, userId);
}
@@ -61,7 +61,7 @@ public class BelongController extends AuthController {
defaultValue = "1") int pageNo,
@RequestParam(name = "page_size", required = false,
defaultValue = "10") int pageSize) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.belongService.listPage(client, graphSpace, roleId, userId,
pageNo, pageSize);
}
@@ -69,14 +69,14 @@ public class BelongController extends AuthController {
@GetMapping("{id}")
public BelongEntity get(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String belongId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.belongService.get(client, graphSpace, belongId);
}
@PostMapping
public void create(@PathVariable("graphspace") String graphSpace,
@RequestBody BelongEntity belongEntity) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
this.belongService.add(client, graphSpace, belongEntity.getRoleId(),
belongEntity.getUserId());
}
@@ -84,7 +84,7 @@ public class BelongController extends AuthController {
@PostMapping("ids")
public void createMany(@PathVariable("graphspace") String graphSpace,
@RequestBody BelongService.BelongsReq belongsReq) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
for (String userId : belongsReq.getUserIds()) {
this.belongService.add(client, graphSpace,
belongsReq.getRoleId(), userId);
@@ -94,7 +94,7 @@ public class BelongController extends AuthController {
@DeleteMapping("{id}")
public void delete(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String belongId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
this.belongService.deleteById(client, graphSpace, belongId);
}
@@ -102,7 +102,7 @@ public class BelongController extends AuthController {
public void delete(@PathVariable("graphspace") String graphSpace,
@RequestParam("role_id") String roleId,
@RequestParam("user_id") String userId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
if (StringUtils.isNotEmpty(roleId) && StringUtils.isNotEmpty(userId)) {
this.belongService.delete(client, graphSpace, roleId, userId);
}
@@ -111,7 +111,7 @@ public class BelongController extends AuthController {
@PostMapping("delids")
public void deleteMany(@PathVariable("graphspace") String graphSpace,
@RequestBody DelIdsReq delIdsReq) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
this.belongService.deleteMany(client, graphSpace,
delIdsReq.ids.toArray(new String[0]));
}
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 8ec43ae5a..a4a9832f0 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
@@ -18,6 +18,8 @@
package org.apache.hugegraph.controller.auth;
+import java.util.Map;
+
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.hugegraph.common.Constant;
import org.apache.hugegraph.driver.HugeClient;
@@ -86,6 +88,15 @@ public class GraphSpaceUserController extends AuthController {
return client.auth().addSpaceAdmin(userId, graphSpace);
}
+ @PutMapping("{id}/preset")
+ public void setPermissionPreset(
+ @PathVariable("graphspace") String graphSpace,
+ @PathVariable("id") String userId,
+ @RequestBody Map<String, String> body) {
+ HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ this.userService.applySpacePreset(client, graphSpace, userId, body.get("permission_preset"));
+ }
+
@DeleteMapping("spaceadmin/{id}")
public void removeGraphSpaceAdmin(
@PathVariable("graphspace") String graphSpace,
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
index 3400d066b..c5a51e226 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java
@@ -75,7 +75,7 @@ public class LoginController extends BaseController {
@PostMapping("/login")
public Object login(@RequestBody Login login) {
- if (this.authMode.anonymous()) {
+ if (this.authMode != null && this.authMode.anonymous()) {
throw new ExternalException(HttpStatus.FORBIDDEN.value(),
"Authentication is disabled");
}
@@ -229,6 +229,9 @@ public class LoginController extends BaseController {
@GetMapping("/status")
public Object status() {
+ if (this.authMode != null && this.authMode.anonymous()) {
+ return ImmutableMap.of("level", "ANONYMOUS");
+ }
HugeClient client = authClient(null, null);
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/RoleController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/RoleController.java
index 06e0e268f..f7ed393f6 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/RoleController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/RoleController.java
@@ -46,7 +46,7 @@ public class RoleController extends AuthController {
@GetMapping("list")
public List<Role> listName(@PathVariable("graphspace") String graphSpace) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.roleService.list(client, graphSpace,
this.userService.isSuperAdmin(client));
}
@@ -60,7 +60,7 @@ public class RoleController extends AuthController {
defaultValue = "1") int pageNo,
@RequestParam(name = "page_size", required = false,
defaultValue = "10") int pageSize) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.roleService.queryPage(
client, graphSpace, query, pageNo, pageSize,
this.userService.isSuperAdmin(client));
@@ -69,7 +69,7 @@ public class RoleController extends AuthController {
@GetMapping("{id}")
public Role get(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String roleId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.roleService.get(client, graphSpace, roleId,
this.userService.isSuperAdmin(client));
}
@@ -77,7 +77,7 @@ public class RoleController extends AuthController {
@PostMapping
public Role add(@PathVariable("graphspace") String graphSpace,
@RequestBody Role role) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
role.graphSpace(graphSpace);
return this.roleService.insert(client, graphSpace, role);
}
@@ -86,7 +86,7 @@ public class RoleController extends AuthController {
public Role update(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String id,
@RequestBody Map<String, Object> body) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
boolean includeLegacy = this.userService.isSuperAdmin(client);
Role current = this.roleService.get(client, graphSpace, id,
includeLegacy);
@@ -108,7 +108,7 @@ public class RoleController extends AuthController {
@DeleteMapping("{id}")
public void delete(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String id) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
this.roleService.delete(client, graphSpace, id,
this.userService.isSuperAdmin(client));
}
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/TargetController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/TargetController.java
index a11368d11..a3d25e3c5 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/TargetController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/TargetController.java
@@ -45,7 +45,7 @@ public class TargetController extends AuthController {
@GetMapping("list")
public List<Target> list(@PathVariable("graphspace") String graphSpace) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.targetService.list(client, graphSpace);
}
@@ -58,7 +58,7 @@ public class TargetController extends AuthController {
defaultValue = "1") int pageNo,
@RequestParam(name = "page_size", required = false,
defaultValue = "10") int pageSize) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.targetService.queryPage(client, graphSpace, query, pageNo,
pageSize);
}
@@ -66,14 +66,14 @@ public class TargetController extends AuthController {
@GetMapping("{id}")
public Target get(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String targetId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.targetService.get(client, graphSpace, targetId);
}
@PostMapping
public Target add(@PathVariable("graphspace") String graphSpace,
@RequestBody Target target) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
return this.targetService.add(client, graphSpace, target);
}
@@ -81,7 +81,7 @@ public class TargetController extends AuthController {
public Target update(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String targetId,
@RequestBody Target target) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
Target current = this.targetService.get(client, graphSpace, targetId);
current.resources(target.resources());
current.description(target.description());
@@ -91,7 +91,7 @@ public class TargetController extends AuthController {
@DeleteMapping("{id}")
public void delete(@PathVariable("graphspace") String graphSpace,
@PathVariable("id") String targetId) {
- HugeClient client = this.requireGraphSpaceManager(graphSpace);
+ HugeClient client = this.requireGraphSpaceAuthorizationAdmin(graphSpace);
this.targetService.delete(client, graphSpace, targetId);
}
}
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 b8c3bc712..fb1d8900a 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
@@ -49,6 +49,7 @@ 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")
@@ -101,6 +102,11 @@ public class GraphSpaceController extends BaseController {
return ImmutableMap.of("records", Collections.emptyList(),
"total", 0);
}
+ 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);
+ }
if (all) {
HugeClient client = this.authClient(null, null);
return this.userService.isSuperAdmin(client) ?
@@ -137,6 +143,12 @@ public class GraphSpaceController extends BaseController {
return this.graphSpaceService.toView(stub);
}
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);
+ }
// Get GraphSpace Info
return graphSpaceService.toView(
graphSpaceService.getWithAdmins(client, graphspace));
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java
index 2bdc11f5a..9db4a23a4 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/CustomInterceptor.java
@@ -19,6 +19,8 @@
package org.apache.hugegraph.handler;
import java.util.regex.Pattern;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -61,6 +63,9 @@ public class CustomInterceptor extends HandlerInterceptorAdapter {
validatePage(request, "page_no", false);
validatePage(request, "page_size", true);
String url = request.getRequestURI();
+ if (url.endsWith("/config")) {
+ return true;
+ }
if (!CHECK_API_PATTERN.matcher(url).matches()) {
setHugeClientToRequest(request);
return true;
@@ -122,24 +127,20 @@ public class CustomInterceptor extends HandlerInterceptorAdapter {
if (this.isLogoutRequest(uri)) {
return;
}
- if (this.authMode.anonymous()) {
- client = unauthClient();
+ if (this.authMode != null && this.authMode.anonymous() &&
+ uri.endsWith("/auth/status")) {
+ return;
+ }
+ String[] scope = this.requestScope(uri);
+ String graphSpace = scope[0];
+ String graph = scope[1];
+ if (this.authMode != null && this.authMode.anonymous()) {
+ client = unauthClient(graphSpace, graph);
} else if (!this.hasAuthSession(request)) {
return;
} else {
String token =
(String) request.getSession().getAttribute(Constant.TOKEN_KEY);
- String [] res = uri.split("/");
- String graphSpace = null;
- String graph = null;
- for (int i = 0; i < res.length; i++) {
- if ("graphspaces".equals(res[i]) && i < res.length - 1) {
- graphSpace = res[i + 1];
- }
- if ("graphs".equals(res[i]) && i < res.length - 1) {
- graph = res[i + 1];
- }
- }
client = this.authClient(graphSpace, graph, token);
}
}
@@ -180,4 +181,33 @@ public class CustomInterceptor extends HandlerInterceptorAdapter {
protected HugeClient unauthClient() {
return this.hugeClientPoolService.createUnauthClient();
}
+
+ protected HugeClient unauthClient(String graphSpace, String graph) {
+ return this.hugeClientPoolService.createUnauthClient(graphSpace, graph);
+ }
+
+ private String[] requestScope(String uri) {
+ String graphSpace = null;
+ String graph = null;
+ String[] parts = uri.split("/");
+ for (int i = 0; i < parts.length; i++) {
+ if ("graphspaces".equals(parts[i]) && i < parts.length - 1) {
+ graphSpace = parts[i + 1];
+ graphSpace = decodeSegment(graphSpace);
+ }
+ if ("graphs".equals(parts[i]) && i < parts.length - 1) {
+ graph = parts[i + 1];
+ graph = decodeSegment(graph);
+ }
+ }
+ return new String[]{graphSpace, graph};
+ }
+
+ private static String decodeSegment(String segment) {
+ try {
+ return URLDecoder.decode(segment, StandardCharsets.UTF_8.name());
+ } catch (java.io.UnsupportedEncodingException ignored) {
+ return segment;
+ }
+ }
}
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 779c65c62..5d49aa9f8 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/handler/LoginInterceptor.java
@@ -20,7 +20,9 @@ package org.apache.hugegraph.handler;
import org.apache.hugegraph.common.Constant;
import org.apache.hugegraph.exception.UnauthorizedException;
+import org.apache.hugegraph.exception.ExternalException;
import org.apache.hugegraph.service.auth.AuthModeService;
+import org.springframework.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@@ -41,6 +43,10 @@ public class LoginInterceptor extends HandlerInterceptorAdapter {
return true;
}
if (this.authMode != null && this.authMode.anonymous()) {
+ if (isAnonymousAuthManagement(request.getRequestURI())) {
+ throw new ExternalException(HttpStatus.FORBIDDEN.value(),
+ "Authentication is disabled");
+ }
return true;
}
@@ -57,4 +63,14 @@ public class LoginInterceptor extends HandlerInterceptorAdapter {
Object value = request.getSession().getAttribute(key);
return value instanceof String && StringUtils.hasText((String) value);
}
+
+ private static boolean isAnonymousAuthManagement(String uri) {
+ if (uri.contains("/graphspaces/") && uri.contains("/auth/")) {
+ return true;
+ }
+ if (!uri.contains("/auth/")) {
+ return false;
+ }
+ return !uri.endsWith("/auth/context") && !uri.endsWith("/auth/status") && !uri.endsWith("/auth/logout");
+ }
}
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java
index 7cb589d1a..3ae56c344 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java
@@ -280,10 +280,8 @@ public class HubbleOptions extends OptionHolder {
);
public static final ConfigOption<Boolean> AUTH_ENABLED =
- new ConfigOption<>(
- "auth.enabled",
- "Whether Hubble requires a user session. Set false when " +
- "the connected HugeGraph Server runs in anonymous mode.",
+ new ConfigOption<>("auth.enabled",
+ "Whether Hubble requires a user session. Set false when " + "the connected HugeGraph Server runs in anonymous mode.",
null,
true
);
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java
index ccd3b7033..167f7f5c0 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java
@@ -91,6 +91,10 @@ public final class HugeClientPoolService {
return getOrCreate(null, null, null, null);
}
+ public HugeClient createUnauthClient(String graphSpace, String graph) {
+ return getOrCreate(null, graphSpace, graph, null);
+ }
+
public HugeClient createTempTokenClient(String token) {
return getOrCreate(null, null, null, token);
}
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 b141b55de..11cd95d8e 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
@@ -87,7 +87,8 @@ public class AuthContextService {
}
public Map<String, Object> context(HugeClient client, String username) {
- if (!this.config.get(HubbleOptions.AUTH_ENABLED)) {
+ if (Boolean.FALSE.equals(
+ this.config.get(HubbleOptions.AUTH_ENABLED))) {
return anonymousContext(this.config.get(HubbleOptions.PD_ENABLED));
}
boolean pdEnabled = this.config.get(HubbleOptions.PD_ENABLED);
@@ -136,10 +137,8 @@ public class AuthContextService {
context.put("username", null);
context.put("role", "ANONYMOUS");
Map<String, Set<String>> actions = new LinkedHashMap<>();
- actions.put("graphspaces", pdEnabled ?
- Collections.singleton("read") : Collections.emptySet());
- context.put("capabilities", pdEnabled ?
- set(GRAPH_RESOURCES_ACCESS, GRAPHSPACES_READ) :
+ actions.put("graphspaces", pdEnabled ? Collections.singleton("read") : Collections.emptySet());
+ context.put("capabilities", pdEnabled ? set(GRAPH_RESOURCES_ACCESS, GRAPHSPACES_READ) :
Collections.singleton(GRAPH_RESOURCES_ACCESS));
context.put("actions", actions);
Map<String, Object> scopes = new LinkedHashMap<>();
@@ -166,6 +165,8 @@ public class AuthContextService {
if (pdEnabled && (SUPERADMIN.equals(role) ||
SPACEADMIN.equals(role))) {
capabilities.add(GRAPHSPACE_MEMBERS_MANAGE);
+ }
+ if (pdEnabled && SUPERADMIN.equals(role)) {
capabilities.add(GRAPHSPACE_ROLES_MANAGE);
capabilities.add(GRAPHSPACE_AUTHORIZATIONS_MANAGE);
}
@@ -185,8 +186,8 @@ public class AuthContextService {
actions.put("graphspaces", pdEnabled ?
(superAdmin ? CRUD_ACTIONS : set("read")) : emptySet());
actions.put("members", spaceManager ? MEMBER_ACTIONS : emptySet());
- actions.put("roles", spaceManager ? CRUD_ACTIONS : emptySet());
- actions.put("authorizations", spaceManager ?
+ actions.put("roles", superAdmin ? CRUD_ACTIONS : emptySet());
+ actions.put("authorizations", superAdmin ?
AUTHORIZATION_ACTIONS : emptySet());
actions.put("operations", superAdmin ?
OPERATIONS_ACTIONS : emptySet());
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java
index 9500b709d..f4f8c8230 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java
@@ -38,7 +38,7 @@ public final class AuthModeService {
}
public boolean enabled() {
- return this.config.get(HubbleOptions.AUTH_ENABLED);
+ return !Boolean.FALSE.equals(this.config.get(HubbleOptions.AUTH_ENABLED));
}
public boolean anonymous() {
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 ad8a669fa..225a1b535 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
@@ -20,9 +20,7 @@ package org.apache.hugegraph.service.auth;
import java.util.ArrayList;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -35,7 +33,6 @@ import org.apache.hugegraph.driver.HugeClient;
import org.apache.hugegraph.entity.auth.BelongEntity;
import org.apache.hugegraph.entity.auth.RoleEntity;
import org.apache.hugegraph.entity.auth.UserView;
-import org.apache.hugegraph.exception.ExternalException;
import org.apache.hugegraph.structure.auth.User;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.PageUtil;
@@ -48,11 +45,9 @@ public class GraphSpaceUserService extends AuthService {
@Autowired
private BelongService belongService;
- @Autowired
- private RoleService roleService;
public List<UserView> listUsers(HugeClient client, String graphSpace) {
- List<UserView> users = new ArrayList<>();
+ Map<String, UserView> users = new java.util.LinkedHashMap<>();
List<BelongEntity> belongs = this.belongService.list(
client, graphSpace, null, null);
@@ -69,9 +64,22 @@ public class GraphSpaceUserService extends AuthService {
user.addRole(new RoleEntity(belong.getRoleId(),
belong.getRoleName()));
});
- users.add(user);
+ users.put(userId, user);
});
- return users;
+ client.auth().listSpaceMember(graphSpace).forEach(username -> {
+ User account = client.findUserByName(username);
+ if (account == null) {
+ return;
+ }
+ String userId = account.id().toString();
+ UserView user = users.computeIfAbsent(userId,
+ id -> new UserView(id, username, new ArrayList<>()));
+ if (client.supportsDefaultRole()) {
+ this.addDefaultRole(client, graphSpace, user, username, "observer");
+ this.addDefaultRole(client, graphSpace, user, username, "analyst");
+ }
+ });
+ return new ArrayList<>(users.values());
}
public UserView getUser(HugeClient client, String graphSpace,
@@ -86,6 +94,19 @@ public class GraphSpaceUserService extends AuthService {
user.addRole(new RoleEntity(belong.getRoleId(),
belong.getRoleName()));
});
+ if (user.getId() == null) {
+ User account = client.auth().getUser(userId);
+ if (account != null) {
+ user.setId(account.id().toString());
+ user.setName(account.name());
+ if (client.supportsDefaultRole()) {
+ this.addDefaultRole(client, graphSpace, user,
+ account.name(), "observer");
+ this.addDefaultRole(client, graphSpace, user,
+ account.name(), "analyst");
+ }
+ }
+ }
return user;
}
@@ -147,82 +168,151 @@ public class GraphSpaceUserService extends AuthService {
if (preset == null || "SUPER_ADMIN".equals(preset)) {
return;
}
- User account = client.auth().getUserByName(username);
+ E.checkArgument(client.supportsDefaultRole(), "Permission presets require HugeGraph Server 1.8+");
+ User account = client.findUserByName(username);
if (account == null) {
return;
}
- Map<String, String> desired = new HashMap<>();
- List<Map<String, String>> requested = permissions == null ?
- new ArrayList<>() : permissions;
+ Map<String, String> desired = new java.util.LinkedHashMap<>();
+ List<Map<String, String>> requested = permissions == null ? new ArrayList<>() : permissions;
for (Map<String, String> permission : requested) {
String graphSpace = permission.get("graphspace");
String permissionPreset = permission.get("permission_preset");
if (graphSpace != null) {
- desired.put(graphSpace,
- "GS_READ_ONLY".equals(permissionPreset) ?
- "observer" : "GS_READ_WRITE".equals(permissionPreset) ?
- "analyst" : null);
+ desired.put(graphSpace, permissionPreset);
}
}
for (String graphSpace : client.graphSpace().listGraphSpace()) {
- UserView current = this.getUser(client, graphSpace,
- account.id().toString());
- if (!desired.containsKey(graphSpace)) {
- if (!current.getRoles().isEmpty() &&
- current.getRoles().stream().allMatch(
- GraphSpaceUserService::isPresetRole)) {
- this.unauthUser(client, graphSpace,
- account.id().toString());
- }
- continue;
+ String desiredPreset = desired.get(graphSpace);
+ if (desiredPreset == null) {
+ this.unauthUser(client, graphSpace,
+ account.id().toString());
+ } else {
+ this.applySpacePreset(client, graphSpace,
+ account.id().toString(),
+ desiredPreset);
}
- String presetRole = desired.get(graphSpace);
- if (presetRole == null) {
- List<RoleEntity> customRoles = current.getRoles().stream()
- .filter(existing -> !isPresetRole(existing))
- .collect(Collectors.toList());
- if (customRoles.isEmpty() && !current.getRoles().isEmpty()) {
- this.unauthUser(client, graphSpace,
- account.id().toString());
- } else if (!customRoles.isEmpty()) {
- UserView view = new UserView(account.id().toString(),
- username, customRoles);
- this.createOrUpdate(client, graphSpace, view);
- }
- continue;
+ }
+ }
+
+ public void validatePermissionPresets(
+ HugeClient client, List<Map<String, String>> permissions,
+ String preset) {
+ if (preset == null || "SUPER_ADMIN".equals(preset)) {
+ return;
+ }
+ E.checkArgument(client.supportsDefaultRole(), "Permission presets require HugeGraph Server 1.8+");
+ Set<String> graphSpaces =
+ new java.util.HashSet<>(client.graphSpace().listGraphSpace());
+ for (Map<String, String> permission :
+ permissions == null ? new ArrayList<Map<String, String>>() : permissions) {
+ String graphSpace = permission.get("graphspace");
+ String permissionPreset = permission.get("permission_preset");
+ E.checkArgument(graphSpace != null && graphSpaces.contains(graphSpace),
+ "The graphspace does not exist: %s", graphSpace);
+ E.checkArgument("GS_READ_ONLY".equals(permissionPreset) ||
+ "GS_READ_WRITE".equals(permissionPreset) || "GS_ADMIN".equals(permissionPreset),
+ "Unsupported permission preset: %s",
+ permissionPreset);
+ }
+ }
+
+ public void applySpacePreset(HugeClient client, String graphSpace,
+ String userId, String preset) {
+ E.checkArgument("GS_READ_ONLY".equals(preset) || "GS_READ_WRITE".equals(preset) || "GS_ADMIN".equals(preset),
+ "Unsupported permission preset: %s", preset);
+ E.checkArgument(client.supportsDefaultRole(),
+ "Permission presets require HugeGraph Server 1.8+");
+ User account = client.auth().getUser(userId);
+ E.checkNotNull(account, "User");
+ this.clearCustomRoles(client, graphSpace, userId);
+ this.clearDefaultRoles(client, graphSpace, account.name());
+ if (!client.auth().listSpaceMember(graphSpace)
+ .contains(account.name())) {
+ client.auth().addSpaceMember(account.name(), graphSpace);
+ }
+ if ("GS_ADMIN".equals(preset)) {
+ if (!client.auth().listSpaceAdmin(graphSpace)
+ .contains(account.name())) {
+ client.auth().addSpaceAdmin(account.name(), graphSpace);
}
- String role = this.resolvePresetRole(client, graphSpace,
- presetRole);
- UserView view = new UserView(account.id().toString(), username,
- new ArrayList<>());
- view.addRole(new RoleEntity(role, role));
- current.getRoles().stream()
- .filter(existing -> !isPresetRole(existing))
- .forEach(view::addRole);
- this.createOrUpdate(client, graphSpace, view);
+ this.setDefaultRole(client, graphSpace, account.name(), "analyst");
+ return;
}
+ if (client.auth().listSpaceAdmin(graphSpace).contains(account.name())) {
+ client.auth().delSpaceAdmin(account.name(), graphSpace);
+ }
+ String role = "GS_READ_ONLY".equals(preset) ? "observer" : "analyst";
+ this.setDefaultRole(client, graphSpace, account.name(), role);
+ }
+
+ public void removeSpacePreset(HugeClient client, String graphSpace,
+ String userId) {
+ this.unauthUser(client, graphSpace, userId);
}
- private String resolvePresetRole(HugeClient client, String graphSpace,
- String roleName) {
- return this.roleService.list(client, graphSpace, true).stream()
- .filter(role -> roleName.equalsIgnoreCase(role.name()) ||
- roleName.equalsIgnoreCase(role.nickname()))
- .map(role -> role.id().toString())
- .findFirst()
- .orElseThrow(() -> new ExternalException(
- "auth.role.not-exist", roleName));
+ public boolean hasCustomRoles(HugeClient client, String graphSpace,
+ String userId) {
+ return !this.belongService.list(client, graphSpace, null, userId).isEmpty();
}
- private static boolean isPresetRole(RoleEntity role) {
- String name = role.getName() == null ? role.getId() :
- role.getName();
- if (name == null) {
+ public boolean hasGraphSpaceAccess(HugeClient client, String graphSpace,
+ String username) {
+ if (!client.supportsDefaultRole()) {
return false;
}
- String normalized = name.toLowerCase(Locale.ROOT);
- return "observer".equals(normalized) ||
- "analyst".equals(normalized);
+ if (client.graphSpace().checkDefaultRole(
+ graphSpace, username, "analyst")) {
+ return true;
+ }
+ return this.graphs(client, graphSpace).stream().anyMatch(
+ graph -> client.graphSpace().checkDefaultRole(graphSpace, username, "observer", graph));
+ }
+
+ private void clearDefaultRoles(HugeClient client, String graphSpace,
+ String username) {
+ if (client.graphSpace().checkDefaultRole(
+ graphSpace, username, "analyst")) {
+ client.graphSpace().deleteDefaultRole(graphSpace, username, "analyst");
+ }
+ for (String graph : this.graphs(client, graphSpace)) {
+ if (client.graphSpace().checkDefaultRole(
+ graphSpace, username, "observer", graph)) {
+ client.graphSpace().deleteDefaultRole(graphSpace, username, "observer", graph);
+ }
+ }
+ }
+
+ private void clearCustomRoles(HugeClient client, String graphSpace,
+ String userId) {
+ this.belongService.list(client, graphSpace, null, userId)
+ .forEach(belong -> this.belongService.deleteById(client, graphSpace, belong.getId()));
+ }
+
+ private void setDefaultRole(HugeClient client, String graphSpace,
+ String username, String role) {
+ if ("observer".equals(role)) {
+ for (String graph : this.graphs(client, graphSpace)) {
+ client.graphSpace().setDefaultRole(graphSpace, username, role, graph);
+ }
+ return;
+ }
+ client.graphSpace().setDefaultRole(graphSpace, username, role);
+ }
+
+ private void addDefaultRole(HugeClient client, String graphSpace,
+ UserView user, String username, String role) {
+ boolean assigned = "observer".equals(role) ? this.graphs(client, graphSpace).stream().anyMatch(
+ graph -> client.graphSpace().checkDefaultRole(graphSpace, username, role, graph)) :
+ client.graphSpace().checkDefaultRole(graphSpace, username, role);
+ if (assigned) {
+ user.addRole(new RoleEntity(role, role));
+ }
+ }
+
+ private List<String> graphs(HugeClient client, String graphSpace) {
+ client.assignGraph(graphSpace, "");
+ return client.graphs().listGraph();
}
public void unauthUser(HugeClient client, String graphSpace,
@@ -231,11 +321,18 @@ public class GraphSpaceUserService extends AuthService {
E.checkNotNull(account, "User");
List<BelongEntity> belongs = this.belongService.list(
client, graphSpace, null, userId);
- E.checkState(!belongs.isEmpty(), "The user: (%s) not exists", userId);
belongs.forEach(belong -> {
this.belongService.deleteById(client, graphSpace, belong.getId());
});
- client.auth().delSpaceMember(account.name(), graphSpace);
+ if (client.supportsDefaultRole()) {
+ this.clearDefaultRoles(client, graphSpace, account.name());
+ }
+ if (client.auth().listSpaceAdmin(graphSpace).contains(account.name())) {
+ client.auth().delSpaceAdmin(account.name(), graphSpace);
+ }
+ if (client.auth().listSpaceMember(graphSpace).contains(account.name())) {
+ client.auth().delSpaceMember(account.name(), graphSpace);
+ }
}
public IPage<User> querySpaceAdmins(HugeClient client, String graphSpace,
@@ -253,7 +350,10 @@ public class GraphSpaceUserService extends AuthService {
List<String> spaceAdmins = client.auth().listSpaceAdmin(graphSpace);
ArrayList<User> users = new ArrayList<>();
for (String spaceAdmin : spaceAdmins) {
- users.add(client.auth().getUser(spaceAdmin));
+ User user = client.findUserByName(spaceAdmin);
+ if (user != null) {
+ users.add(user);
+ }
}
return users;
}
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 6e696705f..4e8153a4d 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
@@ -26,7 +26,6 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Locale;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -42,10 +41,9 @@ import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.driver.AuthManager;
import org.apache.hugegraph.driver.HugeClient;
import org.apache.hugegraph.entity.auth.UserEntity;
-import org.apache.hugegraph.entity.auth.RoleEntity;
-import org.apache.hugegraph.entity.auth.UserView;
import org.apache.hugegraph.exception.InternalException;
import org.apache.hugegraph.structure.auth.User;
+import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.HubbleUtil;
import org.apache.hugegraph.util.PageUtil;
@@ -105,7 +103,7 @@ public class UserService extends AuthService {
}
public UserEntity getUser(HugeClient client, String name) {
- return convert(client, client.auth().getUserByName(name));
+ return convert(client, client.findUserByName(name));
}
public Object queryPage(HugeClient hugeClient, String query,
@@ -159,9 +157,13 @@ public class UserService extends AuthService {
Map<String, List<String>> spaceMap =
HubbleUtil.uncheckedCast(listMap.get(0));
List<String> adminSpaces = spaceMap.get(userId);
+ if (adminSpaces == null) {
+ adminSpaces = new ArrayList<>();
+ }
List<String> resSpaces = new ArrayList<>();
for (String space : spaces) {
- if (hugeClient.graphSpace().checkDefaultRole(space, userId, "analyst")) {
+ if (this.graphSpaceUserService.hasGraphSpaceAccess(
+ hugeClient, space, user.name())) {
resSpaces.add(space);
}
}
@@ -180,8 +182,7 @@ public class UserService extends AuthService {
}
public UserEntity getpersonal(HugeClient hugeClient, String username) {
- AuthManager auth = hugeClient.auth();
- User user = auth.getUserByName(username);
+ User user = hugeClient.findUserByName(username);
if (user == null) {
throw new InternalException("auth.user.get.%s Not Exits",
username);
@@ -197,7 +198,7 @@ public class UserService extends AuthService {
adminSpaces.add(space);
}
if (hugeClient.auth().isSpaceAdmin(space) ||
- hugeClient.auth().checkDefaultRole(space, "analyst")) {
+ hasCurrentUserAccess(hugeClient, space)) {
resSpaces.add(space);
}
}
@@ -215,6 +216,10 @@ public class UserService extends AuthService {
}
public void add(HugeClient client, UserEntity ue) {
+ if (isPdEnabled()) {
+ this.graphSpaceUserService.validatePermissionPresets(client, ue.getGraphspacePermissions(),
+ ue.getPermissionPreset());
+ }
User user = new User();
user.name(ue.getName());
user.password(ue.getPassword());
@@ -233,8 +238,7 @@ public class UserService extends AuthService {
}
}
if (isPdEnabled()) {
- this.graphSpaceUserService.applyPermissionPresets(
- client, ue.getName(), ue.getGraphspacePermissions(),
+ this.graphSpaceUserService.applyPermissionPresets(client, ue.getName(), ue.getGraphspacePermissions(),
ue.getPermissionPreset());
}
@@ -364,68 +368,110 @@ public class UserService extends AuthService {
private void populatePermissionPresets(HugeClient client,
UserEntity userEntity) {
List<Map<String, String>> permissions = new ArrayList<>();
+ if (userEntity.isSuperadmin()) {
+ userEntity.setGraphspacePermissions(permissions);
+ userEntity.setPermissionPreset("SUPER_ADMIN");
+ return;
+ }
+ if (!client.supportsDefaultRole()) {
+ userEntity.setGraphspacePermissions(permissions);
+ userEntity.setPermissionPreset("LEGACY_CUSTOM");
+ return;
+ }
List<String> graphSpaces = client.graphSpace().listGraphSpace();
+ boolean legacyCustom = false;
for (String graphSpace : graphSpaces) {
+ legacyCustom |= this.graphSpaceUserService.hasCustomRoles(client, graphSpace, userEntity.getId());
if (userEntity.getAdminSpaces() != null &&
userEntity.getAdminSpaces().contains(graphSpace)) {
permissions.add(permission(graphSpace, "GS_ADMIN"));
continue;
}
- UserView view = this.graphSpaceUserService.getUser(
- client, graphSpace, userEntity.getId());
- view.getRoles().forEach(role -> {
- String name = role.getName() == null ? "" :
- role.getName().toLowerCase(Locale.ROOT);
- if ("observer".equals(name)) {
- permissions.add(permission(graphSpace, "GS_READ_ONLY"));
- } else if ("analyst".equals(name)) {
- permissions.add(permission(graphSpace, "GS_READ_WRITE"));
- }
- });
+ if (client.graphSpace().checkDefaultRole(
+ graphSpace, userEntity.getName(), "analyst")) {
+ permissions.add(permission(graphSpace, "GS_READ_WRITE"));
+ } else if (hasObserverRole(client, graphSpace, userEntity.getName())) {
+ permissions.add(permission(graphSpace, "GS_READ_ONLY"));
+ }
}
userEntity.setGraphspacePermissions(permissions);
+ if (legacyCustom) {
+ userEntity.setPermissionPreset("LEGACY_CUSTOM");
+ }
}
private void populatePermissionPresets(HugeClient client,
Collection<UserEntity> users) {
- Map<String, List<Map<String, String>>> permissions = new HashMap<>();
- for (String graphSpace : client.graphSpace().listGraphSpace()) {
- for (UserView view : this.graphSpaceUserService.listUsers(
- client, graphSpace)) {
- for (RoleEntity role : view.getRoles()) {
- String name = role.getName() == null ? "" :
- role.getName().toLowerCase(Locale.ROOT);
- String preset = "observer".equals(name) ?
- "GS_READ_ONLY" : "analyst".equals(name) ?
- "GS_READ_WRITE" : null;
- if (preset != null) {
- permissions.computeIfAbsent(view.getId(),
- key -> new ArrayList<>()).add(
- permission(graphSpace, preset));
- }
- }
+ if (!client.supportsDefaultRole()) {
+ for (UserEntity user : users) {
+ user.setGraphspacePermissions(new ArrayList<>());
+ user.setPermissionPreset(user.isSuperadmin() ? "SUPER_ADMIN" : "LEGACY_CUSTOM");
}
+ return;
}
+ List<String> graphSpaces = client.graphSpace().listGraphSpace();
for (UserEntity user : users) {
- List<Map<String, String>> values = permissions.getOrDefault(
- user.getId(), new ArrayList<>());
+ List<Map<String, String>> values = new ArrayList<>();
+ if (user.isSuperadmin()) {
+ user.setGraphspacePermissions(values);
+ user.setPermissionPreset("SUPER_ADMIN");
+ continue;
+ }
+ boolean legacyCustom = false;
if (user.getAdminSpaces() != null) {
for (String graphSpace : user.getAdminSpaces()) {
values.add(permission(graphSpace, "GS_ADMIN"));
}
}
+ for (String graphSpace : graphSpaces) {
+ legacyCustom |= this.graphSpaceUserService.hasCustomRoles(client, graphSpace, user.getId());
+ if (user.getAdminSpaces() != null &&
+ user.getAdminSpaces().contains(graphSpace)) {
+ continue;
+ }
+ if (client.graphSpace().checkDefaultRole(
+ graphSpace, user.getName(), "analyst")) {
+ values.add(permission(graphSpace, "GS_READ_WRITE"));
+ } else if (hasObserverRole(client, graphSpace, user.getName())) {
+ values.add(permission(graphSpace, "GS_READ_ONLY"));
+ }
+ }
user.setGraphspacePermissions(values);
+ if (legacyCustom) {
+ user.setPermissionPreset("LEGACY_CUSTOM");
+ }
}
}
private static Map<String, String> permission(String graphSpace,
- String preset) {
+ String preset) {
Map<String, String> permission = new HashMap<>();
permission.put("graphspace", graphSpace);
permission.put("permission_preset", preset);
return permission;
}
+ private static boolean hasObserverRole(HugeClient client,
+ String graphSpace,
+ String username) {
+ client.assignGraph(graphSpace, "");
+ return client.graphs().listGraph().stream().anyMatch(
+ graph -> client.graphSpace().checkDefaultRole(graphSpace, username, "observer", graph));
+ }
+
+ private static boolean hasCurrentUserAccess(HugeClient client,
+ String graphSpace) {
+ if (!client.supportsDefaultRole()) {
+ return false;
+ }
+ if (client.auth().checkDefaultRole(graphSpace, "analyst")) {
+ return true;
+ }
+ client.assignGraph(graphSpace, "");
+ return client.graphs().listGraph().stream().anyMatch(
+ graph -> client.auth().checkDefaultRole(graphSpace, "observer", graph));
+ }
+
protected List<Object> getSpaceAndSpacenum(HugeClient hugeClient) {
AuthManager auth = hugeClient.auth();
List<Object> listMap = new ArrayList<>();
@@ -459,6 +505,10 @@ public class UserService extends AuthService {
}
public void update(HugeClient hugeClient, UserEntity userEntity) {
+ if (isPdEnabled()) {
+ this.graphSpaceUserService.validatePermissionPresets(hugeClient, userEntity.getGraphspacePermissions(),
+ userEntity.getPermissionPreset());
+ }
User user = new User();
user.setId(userEntity.getId());
user.name(userEntity.getName());
@@ -482,8 +532,7 @@ public class UserService extends AuthService {
hugeClient.auth().updateUser(user);
if (isPdEnabled()) {
- this.graphSpaceUserService.applyPermissionPresets(
- hugeClient, userEntity.getName(),
+ this.graphSpaceUserService.applyPermissionPresets(hugeClient, userEntity.getName(),
userEntity.getGraphspacePermissions(),
userEntity.getPermissionPreset());
}
@@ -491,8 +540,7 @@ public class UserService extends AuthService {
public void updatePersonal(HugeClient hugeClient, String username,
String nickname, String description) {
- AuthManager auth = hugeClient.auth();
- User user = auth.getUserByName(username);
+ User user = hugeClient.findUserByName(username);
if (isPdEnabled()) {
user.nickname(nickname);
} else {
@@ -519,7 +567,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.auth().getUserByName(username);
+ User user = hugeClient.findUserByName(username);
user.password(newpwd);
hugeClient.auth().updateUser(user);
return Response.builder()
@@ -531,8 +579,6 @@ public class UserService extends AuthService {
if (!isPdEnabled()) {
return new ArrayList<>();
}
- AuthManager auth = hugeClient.auth();
- List<User> users = auth.listUsers();
List<String> spaces = hugeClient.graphSpace().listGraphSpace();
List<String> adminspace = new ArrayList<String>();
for (String space : spaces) {
@@ -553,14 +599,31 @@ public class UserService extends AuthService {
return;
}
List<String> oldadminspaces = listAdminSpace(hugeClient, username);
+ User account = hugeClient.findUserByName(username);
+ E.checkNotNull(account, "User");
+ if (!hugeClient.supportsDefaultRole()) {
+ for (String adminspace : adminspaces) {
+ if (!oldadminspaces.contains(adminspace)) {
+ hugeClient.auth().addSpaceAdmin(username, adminspace);
+ }
+ }
+ for (String oldadminspace : oldadminspaces) {
+ if (!adminspaces.contains(oldadminspace)) {
+ hugeClient.auth().delSpaceAdmin(username, oldadminspace);
+ }
+ }
+ return;
+ }
for (String adminspace : adminspaces) {
if (!oldadminspaces.contains(adminspace)) {
- hugeClient.auth().addSpaceAdmin(username, adminspace);
+ this.graphSpaceUserService.applySpacePreset(hugeClient, adminspace, account.id().toString(),
+ "GS_ADMIN");
}
}
for (String oldadminspace : oldadminspaces) {
if (!adminspaces.contains(oldadminspace)) {
- hugeClient.auth().delSpaceAdmin(username, oldadminspace);
+ this.graphSpaceUserService.removeSpacePreset(hugeClient, oldadminspace,
+ account.id().toString());
}
}
}
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 b5b85dca5..ebd45a05b 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
@@ -159,8 +159,46 @@ public class GraphSpaceService {
space.getCreateTime().compareTo(after) > 0)
.filter(space -> !space.isAuth() ||
client.auth().isSpaceAdmin(space.getName()) ||
- client.auth().checkDefaultRole(
- space.getName(), "analyst"))
+ 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()));
+ return results;
+ }
+
+ private static boolean hasCurrentUserAccess(HugeClient client,
+ String graphSpace) {
+ if (!client.supportsDefaultRole()) {
+ return false;
+ }
+ if (client.auth().checkDefaultRole(graphSpace, "analyst")) {
+ return true;
+ }
+ client.assignGraph(graphSpace, "");
+ return client.graphs().listGraph().stream().anyMatch(
+ graph -> client.auth().checkDefaultRole(graphSpace, "observer", graph));
+ }
+
+ public List<Map<String, Object>> queryAnonymousGs(HugeClient client,
+ String query,
+ String createTime) {
+ String prefix = query == null ? "" : query;
+ String after = createTime == null ? "" : createTime;
+ List<Map<String, Object>> results = client.graphSpace()
+ .listGraphSpace().stream()
+ .map(client.graphSpace()::getGraphSpace).filter(space -> space != null && (space.getName().contains(prefix) ||
+ space.getNickname() != null && space.getNickname().contains(prefix)))
+ .filter(space -> space.getCreateTime() == null || space.getCreateTime().compareTo(after) > 0)
.map(space -> {
GraphSpaceEntity entity =
GraphSpaceEntity.fromGraphSpace(space);
@@ -200,7 +238,7 @@ public class GraphSpaceService {
* @param graphSpace
* @return
*/
- Map<String, Object> evCount(HugeClient client, String graphSpace) {
+ public Map<String, Object> evCount(HugeClient client, String graphSpace) {
Long vertexTotal = 0L;
Long edgeTotal = 0L;
Map<String, Object> statisticTotal = new HashMap<>();
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java
index 177b04147..9cf151c9e 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java
@@ -140,7 +140,7 @@ public class GraphSpaceAuthMutationAuthorizationTest {
}
@Test
- public void testCurrentSpaceAdminCanManageScopedAuthorizationResources()
+ public void testCurrentSpaceAdminCanManageMembersOnly()
throws Exception {
Mockito.when(this.authorizationService.isAssignSpaceAdmin(
this.client, "SPACE"))
@@ -149,8 +149,11 @@ public class GraphSpaceAuthMutationAuthorizationTest {
Mockito.eq(this.client), Mockito.any()))
.thenReturn("SPACEADMIN");
- this.assertScopedReadsAllowed();
- this.assertScopedCreatesAllowed();
+ this.assertLowLevelReadsForbidden();
+ ReadRoute members = this.memberReadRoute();
+ mvc(members.controller).perform(members.request)
+ .andExpect(status().isOk());
+ this.assertScopedCreatesForbidden();
}
@Test
@@ -191,6 +194,12 @@ public class GraphSpaceAuthMutationAuthorizationTest {
}
}
+ private void assertLowLevelReadsForbidden() throws Exception {
+ for (ReadRoute route : this.lowLevelReadRoutes()) {
+ assertForbidden(mvc(route.controller), route.request);
+ }
+ }
+
private void assertScopedReadsForbidden() throws Exception {
for (ReadRoute route : this.scopedReadRoutes()) {
assertForbidden(mvc(route.controller), route.request);
@@ -198,6 +207,14 @@ public class GraphSpaceAuthMutationAuthorizationTest {
}
private ReadRoute[] scopedReadRoutes() {
+ ReadRoute[] lowLevel = this.lowLevelReadRoutes();
+ return new ReadRoute[]{
+ lowLevel[0], lowLevel[1], lowLevel[2], lowLevel[3],
+ this.memberReadRoute()
+ };
+ }
+
+ private ReadRoute[] lowLevelReadRoutes() {
return new ReadRoute[]{
new ReadRoute(this.prepare(new TestBelongController(
this.client), "belongService",
@@ -214,12 +231,15 @@ public class GraphSpaceAuthMutationAuthorizationTest {
new ReadRoute(this.prepare(new TestTargetController(
this.client), "targetService",
Mockito.mock(TargetService.class)),
- get("/api/v1.3/graphspaces/SPACE/auth/targets")),
- new ReadRoute(this.prepare(new TestGraphSpaceUserController(
+ get("/api/v1.3/graphspaces/SPACE/auth/targets"))
+ };
+ }
+
+ private ReadRoute memberReadRoute() {
+ return new ReadRoute(this.prepare(new TestGraphSpaceUserController(
this.client), "userService",
Mockito.mock(GraphSpaceUserService.class)),
- get("/api/v1.3/graphspaces/SPACE/auth/users"))
- };
+ get("/api/v1.3/graphspaces/SPACE/auth/users"));
}
private void assertScopedCreatesAllowed() throws Exception {
@@ -242,6 +262,24 @@ public class GraphSpaceAuthMutationAuthorizationTest {
.andExpect(status().isOk());
}
+ private void assertScopedCreatesForbidden() throws Exception {
+ BelongController belong = this.prepare(new TestBelongController(
+ this.client), "belongService", Mockito.mock(BelongService.class));
+ AccessController access = this.prepare(new TestAccessController(
+ this.client), "accessService", Mockito.mock(AccessService.class));
+ TargetController target = this.prepare(new TestTargetController(
+ this.client), "targetService", Mockito.mock(TargetService.class));
+
+ assertForbidden(mvc(belong),
+ post("/api/v1.3/graphspaces/SPACE/auth/belongs")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"role_id\":\"r\",\"user_id\":\"u\"}"));
+ assertForbidden(mvc(access),
+ post("/api/v1.3/graphspaces/SPACE/auth/accesses").contentType(MediaType.APPLICATION_JSON).content("{}"));
+ assertForbidden(mvc(target),
+ post("/api/v1.3/graphspaces/SPACE/auth/targets").contentType(MediaType.APPLICATION_JSON).content("{}"));
+ }
+
private <T extends BaseController> T prepare(T controller,
String serviceField,
Object service) {
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 4516f1286..c8aa3634f 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
@@ -35,6 +35,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.apache.hugegraph.controller.BaseController;
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.AccessEntity;
import org.apache.hugegraph.entity.auth.BelongEntity;
@@ -65,12 +67,24 @@ public class GraphSpaceAuthOwnershipTest {
private HugeClient client;
private AuthManager auth;
+ private GraphSpaceManager graphSpace;
+ private GraphsManager graphs;
@Before
public void setup() {
this.client = Mockito.mock(HugeClient.class);
this.auth = Mockito.mock(AuthManager.class);
+ this.graphSpace = Mockito.mock(GraphSpaceManager.class);
+ this.graphs = Mockito.mock(GraphsManager.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);
+ Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+ Mockito.when(this.graphs.listGraph()).thenReturn(Collections.emptyList());
+ Mockito.when(this.auth.listSpaceAdmin(Mockito.anyString()))
+ .thenReturn(Collections.emptyList());
+ Mockito.when(this.auth.listSpaceMember(Mockito.anyString()))
+ .thenReturn(Collections.emptyList());
}
@Test
@@ -451,14 +465,15 @@ public class GraphSpaceAuthOwnershipTest {
public void testGraphSpaceUserRemovalDeletesOnlyScopedBelongs() {
BelongService belongs = Mockito.mock(BelongService.class);
User account = new User();
+ account.setId("user-id");
account.name("graph-user");
Mockito.when(this.auth.getUser("user-id")).thenReturn(account);
+ Mockito.when(this.auth.listSpaceMember("SPACE_A")).thenReturn(Collections.singletonList("graph-user"));
BelongEntity scoped = BelongEntity.builder()
.id("belong-a")
.userId("user-id")
.build();
- Mockito.when(belongs.list(this.client, "SPACE_A", null, "user-id"))
- .thenReturn(Collections.singletonList(scoped));
+ Mockito.when(belongs.list(this.client, "SPACE_A", null, "user-id")).thenReturn(Collections.singletonList(scoped));
GraphSpaceUserService service = new GraphSpaceUserService();
ReflectionTestUtils.setField(service, "belongService", belongs);
@@ -471,6 +486,27 @@ public class GraphSpaceAuthOwnershipTest {
Mockito.verify(this.auth).delSpaceMember("graph-user", "SPACE_A");
}
+ @Test
+ public void testReadOnlyPresetAppliesObserverToEveryGraph() {
+ User account = new User();
+ account.setId("user-id");
+ account.name("graph-user");
+ Mockito.when(this.auth.getUser("user-id")).thenReturn(account);
+ Mockito.when(this.graphs.listGraph())
+ .thenReturn(Arrays.asList("graph-1", "graph-2"));
+ BelongService belongs = Mockito.mock(BelongService.class);
+ Mockito.when(belongs.list(this.client, "SPACE_A", null, "user-id")).thenReturn(Collections.emptyList());
+ GraphSpaceUserService service = new GraphSpaceUserService();
+ ReflectionTestUtils.setField(service, "belongService", belongs);
+
+ service.applySpacePreset(this.client, "SPACE_A", "user-id", "GS_READ_ONLY");
+
+ Mockito.verify(this.auth).addSpaceMember("graph-user", "SPACE_A");
+ Mockito.verify(this.graphSpace).setDefaultRole("SPACE_A", "graph-user", "observer", "graph-1");
+ Mockito.verify(this.graphSpace).setDefaultRole("SPACE_A", "graph-user", "observer", "graph-2");
+ Mockito.verify(this.graphSpace, Mockito.never()).setDefaultRole("SPACE_A", "graph-user", "observer");
+ }
+
@Test
public void testGraphSpaceUserRoleUpdatePreflightsAllRoles() {
List<Group> groups = this.createScopedGroups("SPACE_A", "SPACE_B");
@@ -500,6 +536,7 @@ public class GraphSpaceAuthOwnershipTest {
Mockito.when(this.auth.getGraphSpaceGroup("local-role"))
.thenReturn(group);
User account = new User();
+ account.setId("user-id");
account.name("graph-user");
Mockito.when(this.auth.getUser("user-id")).thenReturn(account);
Mockito.when(this.auth.listSpaceMember("SPACE_A"))
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 d5a68defb..cc4ba00df 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
@@ -86,8 +86,10 @@ public class AuthContextServiceTest {
Assert.assertTrue(capabilities(context).contains(
"graphspace_members_manage"));
Assert.assertTrue(actions(context, "members").contains("add"));
- Assert.assertTrue(actions(context, "roles").contains("update"));
- Assert.assertTrue(actions(context, "authorizations").contains("grant"));
+ Assert.assertTrue(actions(context, "roles").isEmpty());
+ Assert.assertTrue(actions(context, "authorizations").isEmpty());
+ Assert.assertFalse(capabilities(context).contains("graphspace_roles_manage"));
+ Assert.assertFalse(capabilities(context).contains("graphspace_authorizations_manage"));
Assert.assertEquals(Arrays.asList("space-a", "space-b"),
scopes(context).get("admin_graphspaces"));
Assert.assertFalse((Boolean) scopes(context).get("all_graphspaces"));
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 2675fc1a1..64df7fce9 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
@@ -74,6 +74,7 @@ import org.apache.hugegraph.handler.LoginInterceptor;
import org.apache.hugegraph.handler.MessageSourceHandler;
import org.apache.hugegraph.options.HubbleOptions;
import org.apache.hugegraph.service.auth.AuthContextService;
+import org.apache.hugegraph.service.auth.AuthModeService;
import org.apache.hugegraph.service.auth.LoginAttemptGuard;
import org.apache.hugegraph.service.auth.UserService;
import org.apache.hugegraph.structure.auth.Login;
@@ -177,6 +178,40 @@ public class AuthSecurityTest {
null));
}
+ @Test
+ public void testAnonymousModeBlocksAuthManagementButAllowsContext() {
+ LoginInterceptor interceptor = new LoginInterceptor();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.AUTH_ENABLED))
+ .thenReturn(false);
+ AuthModeService mode = new AuthModeService(config);
+ ReflectionTestUtils.setField(interceptor, "authMode", mode);
+
+ MockHttpServletRequest users = new MockHttpServletRequest("GET", "/api/v1.3/auth/users");
+ try {
+ interceptor.preHandle(users, new MockHttpServletResponse(), null);
+ Assert.fail("Expected anonymous auth management to be blocked");
+ } catch (ExternalException forbidden) {
+ Assert.assertEquals(HttpStatus.FORBIDDEN.value(),
+ forbidden.status());
+ }
+
+ MockHttpServletRequest context = new MockHttpServletRequest("GET", "/api/v1.3/auth/context");
+ Assert.assertTrue(interceptor.preHandle(context, new MockHttpServletResponse(), null));
+ }
+
+ @Test
+ public void testConfigBootstrapDoesNotCreateServerClient()
+ throws Exception {
+ TestCustomInterceptor interceptor = new TestCustomInterceptor();
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1.3/config");
+
+ Assert.assertTrue(interceptor.preHandle(request, new MockHttpServletResponse(), null));
+ Assert.assertEquals(0, interceptor.authClients);
+ Assert.assertEquals(0, interceptor.unauthClients);
+ Assert.assertNull(request.getAttribute("hugeClient"));
+ }
+
@Test
public void testCustomInterceptorDoesNotCreateClientForMissingSession()
throws Exception {
@@ -313,6 +348,22 @@ public class AuthSecurityTest {
Assert.assertEquals("token", interceptor.token);
}
+ @Test
+ public void testAnonymousClientUsesGraphSpaceScope() throws Exception {
+ TestCustomInterceptor interceptor = new TestCustomInterceptor();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.AUTH_ENABLED))
+ .thenReturn(false);
+ AuthModeService mode = new AuthModeService(config);
+ ReflectionTestUtils.setField(interceptor, "authMode", mode);
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1.3/graphspaces/SPACE/graphs/graph/schema");
+
+ Assert.assertTrue(interceptor.preHandle(request, new MockHttpServletResponse(), null));
+ Assert.assertEquals(1, interceptor.unauthClients);
+ Assert.assertEquals("SPACE", interceptor.graphSpace);
+ Assert.assertEquals("graph", interceptor.graph);
+ }
+
@Test
public void testCustomInterceptorDoesNotCreateClientForLogout()
throws Exception {
@@ -871,6 +922,15 @@ public class AuthSecurityTest {
this.unauthClients++;
return null;
}
+
+ @Override
+ protected org.apache.hugegraph.driver.HugeClient unauthClient(
+ String graphSpace, String graph) {
+ this.unauthClients++;
+ this.graphSpace = graphSpace;
+ this.graph = graph;
+ return null;
+ }
}
private static void setField(Object object, String name, Object value)
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 ae0319de2..62570641a 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
@@ -32,6 +32,7 @@ import org.apache.hugegraph.driver.AuthManager;
import org.apache.hugegraph.driver.HugeClient;
import org.apache.hugegraph.entity.auth.UserEntity;
import org.apache.hugegraph.options.HubbleOptions;
+import org.apache.hugegraph.service.auth.GraphSpaceUserService;
import org.apache.hugegraph.service.auth.UserService;
import org.apache.hugegraph.structure.auth.User;
@@ -52,6 +53,8 @@ public class UserServiceCompatibilityTest {
.thenReturn(new User());
this.service = new UserService();
ReflectionTestUtils.setField(this.service, "config", this.config);
+ ReflectionTestUtils.setField(this.service, "graphSpaceUserService",
+ Mockito.mock(GraphSpaceUserService.class));
}
@Test
@@ -111,6 +114,7 @@ public class UserServiceCompatibilityTest {
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"));
this.service.updatePersonal(this.client, "user", "display-name",
"description");
diff --git a/hugegraph-hubble/hubble-fe/src/App.test.js b/hugegraph-hubble/hubble-fe/src/App.test.js
index 11805599a..a3ed5534d 100644
--- a/hugegraph-hubble/hubble-fe/src/App.test.js
+++ b/hugegraph-hubble/hubble-fe/src/App.test.js
@@ -19,15 +19,29 @@
import {render, screen} from '@testing-library/react';
import {MemoryRouter} from 'react-router-dom';
import App from './App';
+import * as api from './api';
+
+jest.mock('./api', () => ({
+ config: {
+ getConfig: jest.fn(),
+ },
+}));
jest.mock('./routes', () => ({element}) => (
<div data-testid="app-route">{element}</div>
));
jest.mock('./layout.ant', () => () => <div>Hubble layout</div>);
+jest.mock('./auth/AuthContext', () => ({
+ AuthContextProvider: ({children}) => children,
+}));
-test('wires the Hubble layout into the application router', () => {
+test('wires the Hubble layout into the application router', async () => {
sessionStorage.clear();
+ api.config.getConfig.mockResolvedValue({
+ status: 200,
+ data: {pd_enabled: false, auth_enabled: false},
+ });
render(
<MemoryRouter
future={{v7_startTransition: true, v7_relativeSplatPath: true}}
@@ -35,6 +49,24 @@ test('wires the Hubble layout into the application router', () => {
<App />
</MemoryRouter>
);
- expect(screen.getByTestId('app-route')).toBeInTheDocument();
+ expect(await screen.findByTestId('app-route')).toBeInTheDocument();
expect(screen.getByText('Hubble layout')).toBeInTheDocument();
+ expect(JSON.parse(sessionStorage.getItem('hubble_config_'))).toEqual({pd_enabled: false, auth_enabled: false});
+});
+
+test('shows a retry surface when configuration bootstrap fails', async () => {
+ api.config.getConfig.mockRejectedValue(new Error('offline'));
+
+ render(
+ <MemoryRouter
+ future={{v7_startTransition: true, v7_relativeSplatPath: true}}
+ >
+ <App />
+ </MemoryRouter>
+ );
+
+ expect(await screen.findByRole('alert')).toHaveTextContent(
+ 'Unable to load Hubble configuration.'
+ );
+ expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument();
});
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 8c435cbc6..aaef6bb25 100644
--- a/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js
+++ b/hugegraph-hubble/hubble-fe/src/api/auth-contract.test.js
@@ -105,6 +105,14 @@ describe('auth API contract', () => {
it.each([
['getSpaceMembers', ['A/B', {page_no: 1}], 'get',
'/graphspaces/A%2FB/auth/users'],
+ ['getSpaceAdmins', ['A/B', {page_no: 1}], 'get',
+ '/graphspaces/A%2FB/auth/users/spaceadmin'],
+ ['setSpaceAdmin', ['A/B', 'u/1'], 'post-empty',
+ '/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',
+ '/graphspaces/A%2FB/auth/users/u%2F1/preset'],
['addSpaceMember', ['A/B', {user_id: 'u'}], 'post',
'/graphspaces/A%2FB/auth/users'],
['updateSpaceMember', ['A/B', 'u/1', {roles: []}], 'put',
@@ -151,6 +159,16 @@ describe('auth API contract', () => {
route, expectedParams, config
);
}
+ else if (verb === 'post-empty') {
+ expect(mockRequest.post).toHaveBeenCalledWith(
+ route, undefined, config
+ );
+ }
+ else if (verb === 'put-preset') {
+ expect(mockRequest.put).toHaveBeenCalledWith(
+ route, {permission_preset: args[2]}, config
+ );
+ }
else {
expect(mockRequest[verb]).toHaveBeenCalledWith(
route, args.at(-1), config
diff --git a/hugegraph-hubble/hubble-fe/src/api/auth.js b/hugegraph-hubble/hubble-fe/src/api/auth.js
index 18a5dee05..541c099f8 100644
--- a/hugegraph-hubble/hubble-fe/src/api/auth.js
+++ b/hugegraph-hubble/hubble-fe/src/api/auth.js
@@ -97,6 +97,27 @@ const getSpaceMembers = (graphspace, params, config = {}) => {
return request.get(scopedAuthPath(graphspace, 'users'), {...config, params});
};
+const getSpaceAdmins = (graphspace, params, config = {}) => {
+ return request.get(scopedAuthPath(graphspace, 'users/spaceadmin'),
+ {...config, params});
+};
+
+const setSpaceAdmin = (graphspace, id, config) => {
+ return request.post(scopedAuthPath(graphspace, 'users/spaceadmin', id),
+ undefined, config);
+};
+
+const removeSpaceAdmin = (graphspace, id, config) => {
+ return request.delete(scopedAuthPath(graphspace, 'users/spaceadmin', id),
+ undefined, config);
+};
+
+const setSpacePreset = (graphspace, id, preset, config) => {
+ return request.put(`${scopedAuthPath(graphspace, 'users', id)}/preset`, {
+ permission_preset: preset,
+ }, config);
+};
+
const addSpaceMember = (graphspace, data, config) => {
return request.post(scopedAuthPath(graphspace, 'users'), data, config);
};
@@ -161,6 +182,10 @@ const deleteSpaceAccess = (graphspace, roleId, targetId, config) => {
export {
getSpaceMembers,
+ getSpaceAdmins,
+ setSpaceAdmin,
+ removeSpaceAdmin,
+ setSpacePreset,
addSpaceMember,
updateSpaceMember,
deleteSpaceMember,
diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js
index 6ed39db42..744fae2c0 100644
--- a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js
+++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js
@@ -30,7 +30,7 @@ import {
ClusterOutlined,
} from '@ant-design/icons';
import {Link, useLocation} from 'react-router-dom';
-import {isPdEnabled} from '../../utils/config';
+import {isAuthEnabled, isPdEnabled} from '../../utils/config';
import {getGraphspacePath} from '../../utils/productMode';
import {getPreparationSchemaPath} from '../../utils/dataPreparationNavigation';
import {getSidebarMenuKey} from '../../utils/sidebarNavigation';
@@ -46,11 +46,11 @@ const items = (t, pathname, capabilities = []) => {
const ACCOUNT = {label: <Link to='/account'>{t('home.account')}</Link>, key: 'account'};
// TODO temporary hided the resource and role modules
- let systemList = [MY];
+ let systemList = isAuthEnabled() ? [MY] : [];
if (capabilities.includes('accounts_manage')
|| capabilities.includes('graphspace_members_manage')) {
// systemList = [MY, RESOURCE, ROLE];
- systemList = [MY, ACCOUNT];
+ systemList = isAuthEnabled() ? [MY, ACCOUNT] : [];
}
const operationsList = [
...(pdMode && capabilities.includes('operations_health_read') ? [{
diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js
index 541015d68..db826c47f 100644
--- a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js
+++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js
@@ -73,6 +73,27 @@ test('hides PD-only operations links in standalone mode without topology access'
expect(screen.queryByRole('link', {name: '节点详情'})).not.toBeInTheDocument();
});
+test('hides account and profile links in anonymous mode', async () => {
+ sessionStorage.setItem('hubble_config_', JSON.stringify({
+ pd_enabled: false,
+ auth_enabled: false,
+ }));
+
+ render(
+ <MemoryRouter
+ initialEntries={['/navigation']}
+ future={{v7_startTransition: true, v7_relativeSplatPath: true}}
+ >
+ <Sidebar />
+ </MemoryRouter>
+ );
+
+ expect(await screen.findByRole('navigation', {name: '主导航'}))
+ .toBeInTheDocument();
+ expect(screen.queryByRole('link', {name: '个人中心'})).not.toBeInTheDocument();
+ expect(screen.queryByRole('link', {name: '账号管理'})).not.toBeInTheDocument();
+});
+
test('keeps monitoring and account links in one operations section', async () => {
sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true}));
render(
diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
index e8b92359a..eda395aec 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
@@ -391,7 +391,9 @@
"SUPER_ADMIN": "Super Administrator",
"GS_READ_ONLY": "GraphSpace Read-only",
"GS_READ_WRITE": "GraphSpace Read-write",
- "GS_ADMIN": "GraphSpace Administrator"
+ "GS_ADMIN": "GraphSpace Administrator",
+ "mixed": "Mixed GraphSpace access",
+ "legacy_custom": "Legacy/custom access"
},
"form": {
"title_detail": "View Account",
diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
index da3e7f7e0..fa025bb7e 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
@@ -391,7 +391,9 @@
"SUPER_ADMIN": "超级管理员",
"GS_READ_ONLY": "GraphSpace 只读",
"GS_READ_WRITE": "GraphSpace 读写",
- "GS_ADMIN": "GraphSpace 管理员"
+ "GS_ADMIN": "GraphSpace 管理员",
+ "mixed": "混合 GraphSpace 权限",
+ "legacy_custom": "旧版/自定义权限"
},
"form": {
"title_detail": "查看账号",
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index 8d5d43292..775f23587 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -32,8 +32,7 @@ import {
const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
const DEFAULT_ALLOWED_OPERATIONS = {create: true, edit: true, auth: true};
-const permissionPresetChanged = (prev, next) => (
- prev.permission_preset !== next.permission_preset
+const permissionPresetChanged = (prev, next) => (prev.permission_preset !== next.permission_preset
);
const HelpLabel = ({t, labelKey}) => (
@@ -79,8 +78,7 @@ const EditLayer = ({
}).catch(() => message.error(t('common.msg.operation_failed')));
}, [onCancel, refresh, t]);
const updateUser = useCallback(values => {
- return api.auth.updateUser(
- data.id, toPermissionPayload(values), PAGE_ERROR_CONFIG
+ return api.auth.updateUser(data.id, toPermissionPayload(values), PAGE_ERROR_CONFIG
).then(res => {
if (res.status === 200) {
message.success(t('common.msg.update_success'));
@@ -99,8 +97,7 @@ const EditLayer = ({
...values,
permission_preset: PERMISSION_PRESETS.GS_ADMIN,
});
- return api.auth.updateAdminspace(
- data.id, payload.adminSpaces, PAGE_ERROR_CONFIG
+ return api.auth.updateAdminspace(data.id, payload.adminSpaces, PAGE_ERROR_CONFIG
).then(res => {
if (res.status === 200) {
message.success(t('common.msg.set_success'));
@@ -195,7 +192,9 @@ const EditLayer = ({
form.setFieldsValue({
...res.data,
permission_preset: getAccountPreset(res.data),
- graphspaces: getPresetSpaces(res.data),
+ graphspaces: op === 'auth'
+ ? (res.data?.adminSpaces ?? [])
+ : getPresetSpaces(res.data),
});
}
setDetail(res.data);
@@ -253,14 +252,12 @@ const EditLayer = ({
<Form.Item label={t('account.form.name')} className={style.item}>
{detail.user_nickname}
</Form.Item>
- <Form.Item label={t('account.form.permission_preset')} className={style.item}>
- {t(`account.permission_preset.${getAccountPreset(detail)}`)}
+ <Form.Item label={t('account.form.permission_preset')} className={style.item}> {t(`account.permission_preset.${getAccountPreset(detail) ?? 'mixed'}`)}
</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 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}
@@ -341,8 +338,7 @@ const EditLayer = ({
noStyle
shouldUpdate={permissionPresetChanged}
>
- {({getFieldValue}) => (getFieldValue('permission_preset')
- !== PERMISSION_PRESETS.GS_ADMIN ? null : (
+ {({getFieldValue}) => (getFieldValue('permission_preset') === PERMISSION_PRESETS.SUPER_ADMIN ? null : (
<Form.Item
label={<HelpLabel t={t} labelKey='account.form.graphspaces' />}
name="graphspaces"
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
index cd537c444..d6eb374ee 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
@@ -40,6 +40,31 @@ import {PERMISSION_PRESETS} from './permissionPresets';
const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
const PAGE_PARAMS = {query: '', page_no: 1, page_size: 200};
const responseRecords = response => response?.data?.records ?? [];
+const adminRole = {
+ role_id: PERMISSION_PRESETS.GS_ADMIN,
+ permission_preset: PERMISSION_PRESETS.GS_ADMIN,
+};
+
+const mergeMembersAndAdmins = (members, admins) => {
+ const rows = new Map(members.map(member => [
+ member.user_id,
+ {...member, member_roles: member.roles ?? []},
+ ]));
+ admins.forEach(admin => {
+ const userId = admin.id ?? admin.user_id;
+ const existing = rows.get(userId) ?? {};
+ rows.set(userId, {
+ ...existing,
+ user_id: userId,
+ user_name: admin.name ?? admin.user_name ?? existing.user_name,
+ member_roles: existing.roles ?? existing.member_roles ?? [],
+ roles: [...(existing.roles ?? existing.member_roles ?? []), adminRole],
+ is_space_admin: true,
+ });
+ });
+ return Array.from(rows.values());
+};
+
const rolePreset = role => {
const explicit = role?.permission_preset ?? role?.permissionPreset;
if (Object.values(PERMISSION_PRESETS).includes(explicit)) {
@@ -68,11 +93,17 @@ const rolePreset = role => {
return null;
};
+const rolesPreset = roles => {
+ const values = roles ?? [];
+ const presets = values.map(rolePreset);
+ return values.length > 0 && presets.every(Boolean) && new Set(presets).size === 1 ? presets[0] : null;
+};
+
const roleLabel = (role, t) => {
const preset = rolePreset(role);
return preset
? t(`account.permission_preset.${preset}`)
- : role?.role_name ?? role?.role_nickname ?? t('common.label.unknown');
+ : t('account.permission_preset.legacy_custom');
};
const RowAction = ({row, onAction, children}) => {
@@ -212,20 +243,28 @@ const SpaceAccess = () => {
const loadMembers = useCallback(space => api.auth.getSpaceMembers(
space, PAGE_PARAMS, PAGE_ERROR_CONFIG
), []);
- const loadRoles = useCallback(space => api.auth.getSpaceRoles(
+ const loadAdmins = useCallback(space => api.auth.getSpaceAdmins(
space, PAGE_PARAMS, PAGE_ERROR_CONFIG
), []);
const members = useScopedResource(
graphSpace, contextVersion, loadMembers, responseRecords
);
- const roles = useScopedResource(
- graphSpace, contextVersion, loadRoles, responseRecords
+ const admins = useScopedResource(graphSpace, contextVersion, loadAdmins, responseRecords
);
+ const visibleMembers = {
+ data: mergeMembersAndAdmins(members.data, admins.data),
+ loading: members.loading || admins.loading,
+ error: members.error || admins.error,
+ retry: () => {
+ members.retry();
+ admins.retry();
+ },
+ };
const refreshAll = useCallback(() => {
members.retry();
- roles.retry();
- }, [members, roles]);
+ admins.retry();
+ }, [admins, members]);
const runMutation = useCallback(async (operation, close) => {
if (submitting) {
@@ -253,7 +292,7 @@ const SpaceAccess = () => {
const openMember = useCallback(row => {
memberForm.setFieldsValue({
user_id: row?.user_id,
- permission_preset: rolePreset(row?.roles?.[0]),
+ permission_preset: rolesPreset(row?.roles),
});
setMemberDialog(row ?? {});
}, [memberForm]);
@@ -262,27 +301,13 @@ const SpaceAccess = () => {
memberForm.resetFields();
}, [memberForm]);
const submitMember = useCallback(values => {
- const role = roles.data.find(item => rolePreset(item) === values.permission_preset);
- if (!role) {
- message.error(t('account.space_access.member.preset_unavailable'));
- return;
- }
- const payload = {
- user_id: values.user_id,
- roles: role ? [{
- role_id: role.id,
- role_name: role.role_name ?? role.role_nickname ?? values.permission_preset,
- }] : [],
- };
- const operation = memberDialog?.user_id
- ? () => api.auth.updateSpaceMember(
- graphSpace, memberDialog.user_id, payload, PAGE_ERROR_CONFIG
- )
- : () => api.auth.addSpaceMember(
- graphSpace, payload, PAGE_ERROR_CONFIG
- );
- runMutation(operation, closeMember);
- }, [closeMember, graphSpace, memberDialog, roles.data, runMutation, t]);
+ runMutation(
+ () => api.auth.setSpacePreset(graphSpace, values.user_id, values.permission_preset,
+ PAGE_ERROR_CONFIG
+ ),
+ closeMember
+ );
+ }, [closeMember, graphSpace, runMutation]);
const confirmDelete = useCallback((title, operation) => {
Modal.confirm({
@@ -392,7 +417,7 @@ const SpaceAccess = () => {
key: 'members',
label: t('account.space_access.tabs.members'),
children: table(
- members, memberColumns, 'user_id',
+ visibleMembers, memberColumns, 'user_id',
t('account.space_access.member.add'),
addMember, canAddMember
),
@@ -437,4 +462,5 @@ const SpaceAccess = () => {
);
};
+export {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 a74866cc8..7367678f9 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.test.js
@@ -17,7 +17,7 @@
*/
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react';
-import SpaceAccess from './SpaceAccess';
+import SpaceAccess, {rolesPreset} from './SpaceAccess';
import * as api from '../../api';
let mockAuthContext;
@@ -33,6 +33,10 @@ jest.mock('../../auth/AuthContext', () => ({
jest.mock('../../api', () => ({
auth: {
getSpaceMembers: jest.fn(),
+ getSpaceAdmins: jest.fn(),
+ setSpaceAdmin: jest.fn(),
+ removeSpaceAdmin: jest.fn(),
+ setSpacePreset: jest.fn(),
getSpaceRoles: jest.fn(),
getSpaceTargets: jest.fn(),
getSpaceAccesses: jest.fn(),
@@ -66,11 +70,23 @@ const page = records => ({status: 200, data: {records, total: records.length}});
const setResponses = ({members = [], roles = [], targets = [], accesses = []} = {}) => {
api.auth.getSpaceMembers.mockResolvedValue(page(members));
+ api.auth.getSpaceAdmins.mockResolvedValue(page([]));
api.auth.getSpaceRoles.mockResolvedValue(page(roles));
api.auth.getSpaceTargets.mockResolvedValue(page(targets));
api.auth.getSpaceAccesses.mockResolvedValue({status: 200, data: accesses});
};
+test('requires explicit preset for mixed or legacy member roles', () => {
+ expect(rolesPreset([
+ {role_name: 'observer'},
+ {role_name: 'custom-role'},
+ ])).toBeNull();
+ expect(rolesPreset([
+ {role_name: 'observer'},
+ {role_name: 'analyst'},
+ ])).toBeNull();
+});
+
beforeEach(() => {
jest.clearAllMocks();
mockAuthContext = {
@@ -106,7 +122,8 @@ test('uses only path-scoped APIs for a space administrator', async () => {
expect(api.auth.getSpaceMembers).toHaveBeenCalledWith(
'SPACE_A', expect.any(Object), expect.any(Object)
);
- expect(api.auth.getSpaceRoles).toHaveBeenCalledWith(
+ expect(api.auth.getSpaceRoles).not.toHaveBeenCalled();
+ expect(api.auth.getSpaceAdmins).toHaveBeenCalledWith(
'SPACE_A', expect.any(Object), expect.any(Object)
);
expect(api.auth.getSpaceTargets).not.toHaveBeenCalled();
@@ -169,12 +186,8 @@ test('does not infer mutations when the server grants read-only actions', async
})).not.toBeInTheDocument();
});
-test('maps a selected preset to the authoritative role id when adding a member', async () => {
- api.auth.getSpaceRoles.mockResolvedValueOnce(page([{
- id: 'writer-id',
- permission_preset: 'GS_READ_WRITE',
- }]));
- api.auth.addSpaceMember.mockResolvedValue({status: 200});
+test('submits only the selected preset when adding a member', async () => {
+ api.auth.setSpacePreset.mockResolvedValue({status: 200});
render(<SpaceAccess />);
await screen.findAllByText('alice');
@@ -190,18 +203,13 @@ test('maps a selected preset to the authoritative role id when adding a member',
fireEvent.click(screen.getByText('account.permission_preset.GS_READ_WRITE'));
fireEvent.click(screen.getByRole('button', {name: 'OK'}));
- await waitFor(() => expect(api.auth.addSpaceMember).toHaveBeenCalledWith(
- 'SPACE_A',
- {
- user_id: 'bob',
- roles: [{role_id: 'writer-id', role_name: 'GS_READ_WRITE'}],
- },
- expect.any(Object)
+ await waitFor(() => expect(api.auth.setSpacePreset).toHaveBeenCalledWith(
+ 'SPACE_A', 'bob', 'GS_READ_WRITE', expect.any(Object)
));
});
-test('does not submit when the selected preset has no server role', async () => {
- api.auth.addSpaceMember.mockResolvedValue({status: 200});
+test('uses the preset API for GS admin', async () => {
+ api.auth.setSpacePreset.mockResolvedValue({status: 200});
render(<SpaceAccess />);
await screen.findAllByText('alice');
@@ -214,8 +222,24 @@ test('does not submit when the selected preset has no server role', async () =>
});
const comboboxes = screen.getAllByRole('combobox');
fireEvent.mouseDown(comboboxes[comboboxes.length - 1]);
- fireEvent.click(screen.getByText('account.permission_preset.GS_READ_WRITE'));
+ fireEvent.click(screen.getByText('account.permission_preset.GS_ADMIN'));
fireEvent.click(screen.getByRole('button', {name: 'OK'}));
- await waitFor(() => expect(api.auth.addSpaceMember).not.toHaveBeenCalled());
+ await waitFor(() => expect(api.auth.setSpacePreset).toHaveBeenCalledWith(
+ 'SPACE_A', 'bob', 'GS_ADMIN', expect.any(Object)
+ ));
+ expect(api.auth.addSpaceMember).not.toHaveBeenCalled();
+});
+
+test('shows GraphSpace administrators through the preset model', async () => {
+ api.auth.getSpaceAdmins.mockResolvedValueOnce(page([{
+ id: 'admin-id',
+ name: 'space-admin',
+ }]));
+
+ render(<SpaceAccess />);
+
+ expect(await screen.findByText('space-admin')).toBeInTheDocument();
+ expect(screen.getByText('account.permission_preset.GS_ADMIN'))
+ .toBeInTheDocument();
});
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
index 7c0bd4aae..572795bc4 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
@@ -137,6 +137,9 @@ const GlobalAccounts = () => {
width: 140,
render: row => {
const preset = getAccountPreset(row);
+ if (!preset) {
+ return <Tag>{t('account.permission_preset.mixed')}</Tag>;
+ }
const color = preset === PERMISSION_PRESETS.SUPER_ADMIN ? 'red'
: preset === PERMISSION_PRESETS.GS_ADMIN ? 'blue' : 'default';
return <Tag color={color}>{t(`account.permission_preset.${preset}`)}</Tag>;
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.js b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.js
index 99fb9a64e..072d8a227 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.js
@@ -34,9 +34,21 @@ const getAccountPreset = account => {
if (account?.is_superadmin) {
return PERMISSION_PRESETS.SUPER_ADMIN;
}
- const scopedPresets = (account?.graphspace_permissions ?? [])
+ if (explicit === 'LEGACY_CUSTOM') {
+ return null;
+ }
+ const scopedPermissions = Array.isArray(account?.graphspace_permissions) ? account.graphspace_permissions : [];
+ const scopedPresets = [...scopedPermissions,
+ ...(Array.isArray(account?.adminSpaces)
+ ? account.adminSpaces.map(graphspace => ({
+ graphspace,
+ permission_preset: PERMISSION_PRESETS.GS_ADMIN,
+ })) : [])]
.map(permission => permission?.permission_preset)
.filter(preset => presetKeys.includes(preset));
+ if (new Set(scopedPresets).size > 1) {
+ return null;
+ }
if (scopedPresets.includes(PERMISSION_PRESETS.GS_ADMIN)) {
return PERMISSION_PRESETS.GS_ADMIN;
}
@@ -50,10 +62,22 @@ const getAccountPreset = account => {
};
const getPresetSpaces = account => {
- const spaces = account?.graphspace_permissions ?? account?.adminSpaces;
+ const scoped = Array.isArray(account?.graphspace_permissions)
+ ? account.graphspace_permissions : [];
+ const admins = Array.isArray(account?.adminSpaces)
+ ? account.adminSpaces.map(graphspace => ({
+ graphspace,
+ permission_preset: PERMISSION_PRESETS.GS_ADMIN,
+ })) : [];
+ const spaces = [...scoped, ...admins];
return Array.isArray(spaces)
- ? spaces.map(space => (typeof space === 'string'
- ? space : space.name ?? space.graphspace)).filter(Boolean)
+ ? Array.from(new Set(spaces.map(space => {
+ if (typeof space === 'string') {
+ return space;
+ }
+ const value = space?.name ?? space?.graphspace;
+ return typeof value === 'string' ? value : value?.name;
+ }).filter(Boolean)))
: [];
};
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.test.js b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.test.js
index cf287cae4..bed366348 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/permissionPresets.test.js
@@ -68,3 +68,22 @@ test('clears admin spaces for the super administrator preset', () => {
is_superadmin: true,
});
});
+
+test('requires an explicit choice before replacing mixed GraphSpace presets', () => {
+ expect(getAccountPreset({
+ graphspace_permissions: [
+ {graphspace: 'read', permission_preset: 'GS_READ_ONLY'},
+ {graphspace: 'write', permission_preset: 'GS_READ_WRITE'},
+ ],
+ })).toBeNull();
+});
+
+test('requires an explicit choice before migrating legacy custom access', () => {
+ expect(getAccountPreset({
+ permission_preset: 'LEGACY_CUSTOM',
+ graphspace_permissions: [{
+ graphspace: 'legacy',
+ permission_preset: 'GS_READ_WRITE',
+ }],
+ })).toBeNull();
+});
diff --git a/hugegraph-hubble/hubble-fe/src/routes/index.js b/hugegraph-hubble/hubble-fe/src/routes/index.js
index 8d7a27599..307217715 100644
--- a/hugegraph-hubble/hubble-fe/src/routes/index.js
+++ b/hugegraph-hubble/hubble-fe/src/routes/index.js
@@ -89,7 +89,7 @@ const AccountRoute = () => {
return hasCapability('accounts_manage')
|| hasCapability('graphspace_members_manage')
? <Account />
- : <Navigate to='/profile' replace />;
+ : <Navigate to='/navigation' replace />;
};
const GraphSpaceListRoute = () => {
@@ -102,7 +102,7 @@ const GraphSpaceListRoute = () => {
return null;
}
return hasCapability('graphspaces_read')
- ? <GraphSpace /> : <Navigate to='/profile' replace />;
+ ? <GraphSpace /> : <Navigate to='/navigation' replace />;
};
const LegacyProfileRedirect = () => {
@@ -140,7 +140,12 @@ const GraphspaceParamRoute = ({children, fallback}) => {
const RouteList = ({element}) => {
return (
<Routes>
- <Route path="/login" element={<Login />} />
+ <Route
+ path="/login"
+ element={isAuthEnabled()
+ ? <Login />
+ : <Navigate to='/navigation' replace />}
+ />
<Route
path="/"
element={(
@@ -184,7 +189,12 @@ const RouteList = ({element}) => {
<Route path="/task/edit" element={<TaskEdit />} />
<Route path="/task/detail/:taskid" element={<TaskDetail />} />
- <Route path='/profile' element={<My />} />
+ <Route
+ path='/profile'
+ element={isAuthEnabled()
+ ? <My />
+ : <Navigate to='/navigation' replace />}
+ />
<Route path='/my' element={<LegacyProfileRedirect />} />
<Route path='/resource' element={<Navigate to='/navigation' replace />} />
<Route path='/role' element={<Navigate to='/navigation' replace />} />
diff --git a/hugegraph-hubble/hubble-fe/src/routes/route-guard.test.js b/hugegraph-hubble/hubble-fe/src/routes/route-guard.test.js
index 930013503..070bf67e4 100644
--- a/hugegraph-hubble/hubble-fe/src/routes/route-guard.test.js
+++ b/hugegraph-hubble/hubble-fe/src/routes/route-guard.test.js
@@ -19,7 +19,7 @@
import {render, screen} from '@testing-library/react';
import {MemoryRouter, Outlet, useLocation} from 'react-router-dom';
import RouteList from './index';
-import {isPdEnabled} from '../utils/config';
+import {isAuthEnabled, isPdEnabled} from '../utils/config';
let mockCapabilities = new Set();
@@ -54,6 +54,7 @@ jest.mock('../pages/Test', () => () => <div>test page</div>);
jest.mock('../pages/GraphAnalysis', () => () => <div>graph analysis page</div>);
jest.mock('../pages/AsyncTaskResult', () => () => <div>async task result page</div>);
jest.mock('../utils/config', () => ({
+ isAuthEnabled: jest.fn(() => true),
isPdEnabled: jest.fn(() => false),
}));
jest.mock('../auth/AuthContext', () => ({
@@ -97,6 +98,7 @@ describe('route guard', () => {
sessionStorage.clear();
mockAccountRender.mockClear();
mockCapabilities = new Set();
+ isAuthEnabled.mockReturnValue(true);
isPdEnabled.mockReturnValue(false);
});
@@ -125,6 +127,17 @@ describe('route guard', () => {
expect(sessionStorage.getItem('redirect')).toBeNull();
});
+ it.each(['/login', '/profile'])('hides authentication route %s in anonymous mode',
+ route => {
+ isAuthEnabled.mockReturnValue(false);
+ renderRoutes(route);
+
+ expect(screen.getByText('navigation page')).toBeTruthy();
+ expect(screen.queryByText(/^login:/)).toBeNull();
+ expect(screen.queryByText('my page')).toBeNull();
+ }
+ );
+
it('renders the standard 404 surface for an unknown operations route', () => {
sessionStorage.setItem('user_', JSON.stringify({
id: 'admin',
@@ -188,7 +201,7 @@ describe('route guard', () => {
renderRoutes('/account');
- expect(screen.getByText('my page')).toBeTruthy();
+ expect(screen.getByText('navigation page')).toBeTruthy();
expect(screen.queryByText('account page')).toBeNull();
expect(mockAccountRender).not.toHaveBeenCalled();
});
@@ -231,7 +244,7 @@ describe('route guard', () => {
it.each([
['/graphspace', 'graph page'],
- ['/account', 'my page'],
+ ['/account', 'navigation page'],
])('uses the non-PD fallback for %s', (route, page) => {
sessionStorage.setItem('user_', JSON.stringify({
id: 'admin',
@@ -280,7 +293,7 @@ describe('route guard', () => {
renderRoutes('/account');
- expect(screen.getByText('my page')).toBeTruthy();
+ expect(screen.getByText('navigation page')).toBeTruthy();
expect(screen.queryByText('account page')).toBeNull();
});
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
index 60b785836..99f32095e 100644
--- a/tools/checkstyle.xml
+++ b/tools/checkstyle.xml
@@ -25,7 +25,7 @@
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
- <property name="max" value="100"/>
+ <property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://|Reflection\.*"/>
</module>
<module name="RegexpSingleline">