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:22 UTC
(hugegraph-toolchain) 07/24: fix(hubble): secure scoped GraphSpace workflows
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 de0fd460c07b35d1ae16f6671580164bb0a84372
Author: dark <ji...@apache.org>
AuthorDate: Tue Aug 18 22:49:44 2026 +0800
fix(hubble): secure scoped GraphSpace workflows
- hide backend bootstrap details
- enforce direct and indirect GraphSpace scope
- isolate query records and account security updates
- retain non-auth verification evidence
---
.../images/pr27/15-standalone-nonauth-visual.png | Bin 0 -> 90401 bytes
.../images/pr27/18-standalone-nonauth-visual.png | Bin 0 -> 93329 bytes
.../java/org/apache/hugegraph/common/Constant.java | 2 +
.../hugegraph/controller/BaseController.java | 38 ++++-
.../hugegraph/controller/ConfigController.java | 6 +-
.../controller/graphs/GraphsController.java | 10 +-
.../controller/ingest/IngestController.java | 100 +++++++++++-
.../controller/load/FileMappingController.java | 11 ++
.../controller/load/FileUploadController.java | 4 +
.../controller/load/JobManagerController.java | 6 +-
.../controller/load/LoadTaskController.java | 7 +
.../controller/query/ExecuteHistoryController.java | 9 +-
.../query/GremlinCollectionController.java | 30 +++-
.../controller/space/GraphSpaceController.java | 35 +++--
.../controller/space/VermeerController.java | 2 +-
.../hugegraph/handler/CustomInterceptor.java | 44 +++++-
.../apache/hugegraph/handler/LoginInterceptor.java | 3 +-
.../service/auth/GraphSpaceUserService.java | 3 +-
.../apache/hugegraph/service/auth/UserService.java | 87 +++++-----
.../hugegraph/service/load/JobManagerService.java | 34 ++++
.../service/query/ExecuteHistoryService.java | 20 ++-
.../service/query/GremlinCollectionService.java | 25 ++-
.../hugegraph/service/space/GraphSpaceService.java | 73 +++++++--
.../controller/ingest/IngestControllerTest.java | 118 ++++++++++++++
.../controller/space/GraphSpaceControllerTest.java | 68 ++++++++
.../service/auth/GraphSpaceUserServiceTest.java | 10 +-
.../service/space/GraphSpaceServiceTest.java | 31 ++++
.../apache/hugegraph/unit/AuthSecurityTest.java | 127 +++++++++++++++
.../hugegraph/unit/ConfigControllerTest.java | 49 ++++++
.../hugegraph/unit/FileMappingDeletionTest.java | 27 +++-
.../unit/GraphsControllerCanonicalTest.java | 52 +++++-
.../hugegraph/unit/LoaderScopeControllerTest.java | 29 +++-
.../unit/UserServiceCompatibilityTest.java | 175 +++++++++++++++++----
.../src/api/request-error-semantics.test.js | 28 ++++
hugegraph-hubble/hubble-fe/src/api/request.js | 13 +-
.../src/i18n/resources/en-US/modules/pages.json | 2 +
.../src/i18n/resources/zh-CN/modules/pages.json | 2 +
.../hubble-fe/src/pages/Account/EditLayer.js | 17 +-
.../pages/Account/account-edit-recovery.test.js | 73 +++++++++
.../hubble-fe/src/pages/Operations/NodeDetail.js | 17 +-
.../src/pages/Operations/NodeDetail.test.js | 4 +
41 files changed, 1230 insertions(+), 161 deletions(-)
diff --git a/hugegraph-hubble/docs/images/pr27/15-standalone-nonauth-visual.png b/hugegraph-hubble/docs/images/pr27/15-standalone-nonauth-visual.png
new file mode 100644
index 000000000..0b472dac9
Binary files /dev/null and b/hugegraph-hubble/docs/images/pr27/15-standalone-nonauth-visual.png differ
diff --git a/hugegraph-hubble/docs/images/pr27/18-standalone-nonauth-visual.png b/hugegraph-hubble/docs/images/pr27/18-standalone-nonauth-visual.png
new file mode 100644
index 000000000..13e8bc6cf
Binary files /dev/null and b/hugegraph-hubble/docs/images/pr27/18-standalone-nonauth-visual.png differ
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java
index 7c29e9518..63be9e458 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/common/Constant.java
@@ -58,6 +58,8 @@ public final class Constant {
public static final String TOKEN_KEY = "auth_token";
public static final String USERNAME_KEY = "username";
+ public static final String GRAPHSPACE_ACCESS_KEY =
+ "validated_graphspace";
public static final int NO_LIMIT = -1;
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 82446d910..1f7634785 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
@@ -28,6 +28,7 @@ import org.apache.hugegraph.options.HubbleOptions;
import org.apache.hugegraph.service.auth.UserService;
import org.apache.hugegraph.service.auth.AuthModeService;
import org.apache.hugegraph.service.auth.AuthContextService;
+import org.apache.hugegraph.service.space.GraphSpaceService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.hugegraph.config.HugeConfig;
import org.springframework.beans.factory.annotation.Autowired;
@@ -65,6 +66,8 @@ public abstract class BaseController {
protected AuthModeService authMode;
@Autowired
protected AuthContextService authContextService;
+ @Autowired
+ protected GraphSpaceService graphSpaceAccessService;
public static final String ORDER_ASC = "asc";
public static final String ORDER_DESC = "desc";
@@ -146,12 +149,14 @@ public abstract class BaseController {
HttpServletRequest request = getRequest();
if (request.getAttribute("hugeClient") != null) {
HugeClient client = (HugeClient) request.getAttribute("hugeClient");
+ this.requireGraphSpaceAccess(client, graphSpace);
client.assignGraph(graphSpace, graph);
return client;
}
HugeClient client = this.authMode != null && this.authMode.anonymous() ?
this.hugeClientPoolService.createUnauthClient(graphSpace, graph) :
this.hugeClientPoolService.createAuthClient(graphSpace, graph, this.getToken());
+ this.requireGraphSpaceAccess(client, graphSpace);
if (graphSpace != null || graph != null) {
client.assignGraph(graphSpace, graph);
}
@@ -160,6 +165,7 @@ public abstract class BaseController {
}
protected HugeClient requireAccountManager() {
+ this.requireAuthenticatedAuthorization();
HugeClient client = this.authClient(null, null);
String level = this.userService.userLevel(client, this.getUser());
if (!"ADMIN".equals(level)) {
@@ -169,6 +175,7 @@ public abstract class BaseController {
}
protected HugeClient requireGraphSpaceManager(String graphSpace) {
+ this.requireAuthenticatedAuthorization();
HugeClient client = this.authClient(null, null);
if (!this.userService.isSuperAdmin(client) &&
!this.userService.isAssignSpaceAdmin(client, graphSpace)) {
@@ -181,6 +188,7 @@ public abstract class BaseController {
protected HugeClient requireGraphSpaceWrite(String graphSpace) {
HugeClient client = this.authClient(null, null);
+ this.requireGraphSpaceAccess(client, graphSpace);
this.authContextService.requireGraphSpaceWrite(
client, this.getUser(), graphSpace);
client.assignGraph(graphSpace, null);
@@ -189,6 +197,7 @@ public abstract class BaseController {
protected HugeClient requireGraphSpaceAuthorizationAdmin(
String graphSpace) {
+ this.requireAuthenticatedAuthorization();
HugeClient client = this.authClient(null, null);
if (!this.userService.isSuperAdmin(client)) {
throw new ForbiddenException("Permission denied: manage authorization objects");
@@ -198,6 +207,7 @@ public abstract class BaseController {
}
protected HugeClient requireGraphSpaceAdministrator() {
+ this.requireAuthenticatedAuthorization();
HugeClient client = this.authClient(null, null);
if (!this.userService.isSuperAdmin(client)) {
throw new ForbiddenException(
@@ -294,10 +304,36 @@ public abstract class BaseController {
HugeClient client = hugeClientPoolService.create(url, graphSpace, graph,
this.getToken());
-
+ this.requireGraphSpaceAccess(client, graphSpace);
return client;
}
+ private void requireAuthenticatedAuthorization() {
+ if (this.authMode != null && this.authMode.anonymous()) {
+ throw new ForbiddenException(
+ "Authentication is required for this operation");
+ }
+ }
+
+ protected void requireGraphSpaceAccess(HugeClient client,
+ String graphSpace) {
+ if (graphSpace == null || !config.get(HubbleOptions.PD_ENABLED)) {
+ return;
+ }
+ HttpServletRequest request = getRequest();
+ if (graphSpace.equals(
+ request.getAttribute(Constant.GRAPHSPACE_ACCESS_KEY))) {
+ return;
+ }
+ if (this.authMode != null && this.authMode.anonymous()) {
+ this.graphSpaceAccessService.requirePublicSpace(client,
+ graphSpace);
+ } else {
+ this.graphSpaceAccessService.requireAccessibleSpace(client,
+ graphSpace);
+ }
+ }
+
public String getUrl() {
boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED);
if (!pdEnabled) {
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
index 7815e82e8..8f8f5cb6c 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java
@@ -39,12 +39,8 @@ public class ConfigController {
@GetMapping
public Map<String, Object> getConfig() {
Map<String, Object> result = new HashMap<>();
- boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED);
- result.put("pd_enabled", pdEnabled);
+ result.put("pd_enabled", config.get(HubbleOptions.PD_ENABLED));
result.put("auth_enabled", config.get(HubbleOptions.AUTH_ENABLED));
- if (!pdEnabled) {
- result.put("server_url", config.get(HubbleOptions.SERVER_URL));
- }
return result;
}
}
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java
index 8fb8ac697..8e2bef18f 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java
@@ -333,9 +333,13 @@ public class GraphsController extends BaseController {
public Object clone(@PathVariable("graphspace") String graphspace,
@PathVariable("graph") String graph,
@RequestBody GraphCloneEntity graphCloneEntity) {
- return this.graphsService.clone(this.authClient(graphspace, graph),
- graphCloneEntity.convertMap(graphspace,
- graph));
+ HugeClient client = this.authClient(graphspace, graph);
+ String targetGraphSpace = graphCloneEntity.getGraphSpace() == null ?
+ graphspace :
+ graphCloneEntity.getGraphSpace();
+ this.requireGraphSpaceAccess(client, targetGraphSpace);
+ return this.graphsService.clone(
+ client, graphCloneEntity.convertMap(graphspace, graph));
}
//
//@Data
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
index 7b7f97050..3fadd9af0 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ingest/IngestController.java
@@ -283,7 +283,8 @@ public class IngestController extends BaseController {
mapping.setEdgeMappings(edgeMappings);
GraphConnection connection = this.graphConnection(graphSpace, graph);
- HugeClient client = this.authClient(graphSpace, graph);
+ HugeClient client = this.requireGraphSpaceWrite(graphSpace);
+ client.assignGraph(graphSpace, graph);
LoadTask task = this.jobManagerService.createIngestTask(
job, mapping, connection, client);
Map<String, Object> data = new HashMap<>();
@@ -299,9 +300,7 @@ public class IngestController extends BaseController {
@RequestParam(name = "page_no", required = false, defaultValue = "1") int pageNo,
@RequestParam(name = "page_size", required = false, defaultValue = "10") int pageSize) {
- // list all jobs across all graphspaces - use empty strings to get all
- // We need to query without graphspace/graph filter for the ingest view
- IPage<JobManager> page = jobManagerService.listAll(pageNo, pageSize, query);
+ IPage<JobManager> page = this.visibleJobPage(pageNo, pageSize, query);
IPage<TaskVO> result = page.convert(job -> {
TaskVO vo = new TaskVO();
@@ -367,11 +366,18 @@ public class IngestController extends BaseController {
return Response.builder().status(Constant.STATUS_NOT_FOUND)
.message("Task not found: " + id).build();
}
+ this.requireJobAccess(job);
return Response.builder().status(Constant.STATUS_OK).data(job).build();
}
@DeleteMapping("/tasks/{id}")
public Response deleteTask(@PathVariable("id") int id) {
+ JobManager job = jobManagerService.get(id);
+ if (job == null) {
+ return Response.builder().status(Constant.STATUS_NOT_FOUND)
+ .message("Task not found: " + id).build();
+ }
+ this.requireJobWrite(job);
jobManagerService.remove(id);
return Response.builder().status(Constant.STATUS_OK).build();
}
@@ -383,6 +389,7 @@ public class IngestController extends BaseController {
return Response.builder().status(Constant.STATUS_NOT_FOUND)
.message("Task not found: " + id).build();
}
+ this.requireJobWrite(job);
job.setJobStatus(JobStatus.DEFAULT);
jobManagerService.update(job);
return Response.builder().status(Constant.STATUS_OK).build();
@@ -395,6 +402,7 @@ public class IngestController extends BaseController {
return Response.builder().status(Constant.STATUS_NOT_FOUND)
.message("Task not found: " + id).build();
}
+ this.requireJobWrite(job);
job.setJobStatus(JobStatus.FAILED);
jobManagerService.update(job);
return Response.builder().status(Constant.STATUS_OK).build();
@@ -408,6 +416,12 @@ public class IngestController extends BaseController {
@RequestParam(name = "page_no", required = false, defaultValue = "1") int pageNo,
@RequestParam(name = "page_size", required = false, defaultValue = "10") int pageSize) {
+ JobManager job = jobManagerService.get(taskId);
+ if (job == null) {
+ return Response.builder().status(Constant.STATUS_NOT_FOUND)
+ .message("Task not found: " + taskId).build();
+ }
+ this.requireJobAccess(job);
List<LoadTask> tasks = loadTaskService.taskListByJob(taskId);
// Manual pagination
@@ -453,11 +467,18 @@ public class IngestController extends BaseController {
return Response.builder().status(Constant.STATUS_NOT_FOUND)
.message("Job not found: " + id).build();
}
+ this.requireLoadTaskAccess(task);
return Response.builder().status(Constant.STATUS_OK).data(task).build();
}
@DeleteMapping("/jobs/{id}")
public Response deleteJob(@PathVariable("id") int id) {
+ LoadTask task = loadTaskService.get(id);
+ if (task == null) {
+ return Response.builder().status(Constant.STATUS_NOT_FOUND)
+ .message("Job not found: " + id).build();
+ }
+ this.requireLoadTaskWrite(task);
loadTaskService.remove(id);
return Response.builder().status(Constant.STATUS_OK).build();
}
@@ -466,7 +487,7 @@ public class IngestController extends BaseController {
@GetMapping("/metrics/task")
public Response metricsTask() {
- List<JobManager> all = jobManagerService.listAll();
+ List<JobManager> all = this.visibleJobs("");
all.forEach(jobManagerService::refreshStatus);
long runningOnce = 0;
@@ -506,6 +527,75 @@ public class IngestController extends BaseController {
// ===== Helpers =====
+ private IPage<JobManager> visibleJobPage(int pageNo, int pageSize,
+ String query) {
+ Set<String> graphSpaces = this.visibleGraphSpaces();
+ IPage<JobManager> page = this.jobManagerService.listByGraphSpaces(
+ graphSpaces, pageNo, pageSize, query);
+ page.getRecords().forEach(jobManagerService::refreshStatus);
+ return page;
+ }
+
+ private List<JobManager> visibleJobs(String query) {
+ Set<String> graphSpaces = this.visibleGraphSpaces();
+ List<JobManager> jobs =
+ this.jobManagerService.listByGraphSpaces(graphSpaces);
+ if (StringUtils.isEmpty(query)) {
+ return jobs;
+ }
+ return jobs.stream()
+ .filter(job -> StringUtils.contains(job.getJobName(), query))
+ .collect(Collectors.toList());
+ }
+
+ private Set<String> visibleGraphSpaces() {
+ if (this.config == null ||
+ !this.config.get(HubbleOptions.PD_ENABLED)) {
+ return null;
+ }
+ HugeClient client = this.authClient(null, null);
+ if (this.authMode != null && this.authMode.anonymous()) {
+ return new LinkedHashSet<>(
+ this.graphSpaceAccessService.listAnonymous(client));
+ }
+ if (this.userService.isSuperAdmin(client)) {
+ return null;
+ }
+ return new LinkedHashSet<>(
+ this.graphSpaceAccessService.listAccessible(client));
+ }
+
+ private void requireJobAccess(JobManager job) {
+ if (job == null) {
+ return;
+ }
+ this.requireGraphSpaceAccess(this.authClient(null, null),
+ job.getGraphSpace());
+ }
+
+ private void requireJobWrite(JobManager job) {
+ if (job == null) {
+ return;
+ }
+ this.requireGraphSpaceWrite(job.getGraphSpace());
+ }
+
+ private void requireLoadTaskAccess(LoadTask task) {
+ JobManager job = task.getJobId() == null ? null :
+ this.jobManagerService.get(task.getJobId());
+ Ex.check(job != null, "job-manager.not-exist.id",
+ task.getJobId());
+ this.requireJobAccess(job);
+ }
+
+ private void requireLoadTaskWrite(LoadTask task) {
+ JobManager job = task.getJobId() == null ? null :
+ this.jobManagerService.get(task.getJobId());
+ Ex.check(job != null, "job-manager.not-exist.id",
+ task.getJobId());
+ this.requireJobWrite(job);
+ }
+
/**
* Same format-whitelist check as
* FileUploadController#checkFileValid
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java
index 39674b59e..43381c508 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileMappingController.java
@@ -112,6 +112,7 @@ public class FileMappingController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@PathVariable("id") int id) {
+ this.requireGraphSpaceWrite(graphSpace);
FileMapping mapping = this.service.get(graphSpace, graph, jobId, id);
if (mapping == null) {
throw new ExternalException("load.file-mapping.not-exist.id", id);
@@ -125,6 +126,7 @@ public class FileMappingController extends BaseController {
public void clear(@PathVariable("graphspace") String graphSpace,
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId) {
+ this.requireGraphSpaceWrite(graphSpace);
List<FileMapping> mappings = this.service.listByJob(graphSpace, graph,
jobId);
Set<Integer> fileIds = new HashSet<>();
@@ -143,6 +145,7 @@ public class FileMappingController extends BaseController {
@PathVariable("jobId") int jobId,
@PathVariable("id") int id,
@RequestBody FileSetting newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
Ex.check(!StringUtils.isEmpty(newEntity.getDelimiter()),
"load.file-mapping.file-setting.delimiter-cannot-be-empty");
Ex.check(!StringUtils.isEmpty(newEntity.getCharset()),
@@ -175,6 +178,7 @@ public class FileMappingController extends BaseController {
@PathVariable("jobId") int jobId,
@PathVariable("id") int id,
@RequestBody VertexMapping newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
FileMapping mapping = this.service.get(graphSpace, graph, jobId, id);
if (mapping == null) {
throw new ExternalException("load.file-mapping.not-exist.id", id);
@@ -195,6 +199,7 @@ public class FileMappingController extends BaseController {
@PathVariable("id") int id,
@PathVariable("vmid") String vmId,
@RequestBody VertexMapping newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
FileMapping mapping = this.service.get(graphSpace, graph, jobId, id);
if (mapping == null) {
throw new ExternalException("load.file-mapping.not-exist.id", id);
@@ -221,6 +226,7 @@ public class FileMappingController extends BaseController {
@PathVariable("jobId") int jobId,
@PathVariable("id") int id,
@PathVariable("vmid") String vmid) {
+ this.requireGraphSpaceWrite(graphSpace);
FileMapping mapping = this.service.get(graphSpace, graph, jobId, id);
if (mapping == null) {
throw new ExternalException("load.file-mapping.not-exist.id", id);
@@ -242,6 +248,7 @@ public class FileMappingController extends BaseController {
@PathVariable("jobId") int jobId,
@PathVariable("id") int id,
@RequestBody EdgeMapping newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
FileMapping mapping = this.service.get(graphSpace, graph, jobId, id);
if (mapping == null) {
throw new ExternalException("load.file-mapping.not-exist.id", id);
@@ -262,6 +269,7 @@ public class FileMappingController extends BaseController {
@PathVariable("id") int id,
@PathVariable("emid") String emId,
@RequestBody EdgeMapping newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
FileMapping mapping = this.service.get(graphSpace, graph, jobId, id);
if (mapping == null) {
throw new ExternalException("load.file-mapping.not-exist.id", id);
@@ -288,6 +296,7 @@ public class FileMappingController extends BaseController {
@PathVariable("jobId") int jobId,
@PathVariable("id") int id,
@PathVariable("emid") String emid) {
+ this.requireGraphSpaceWrite(graphSpace);
FileMapping mapping = this.service.get(graphSpace, graph, jobId, id);
if (mapping == null) {
throw new ExternalException("load.file-mapping.not-exist.id", id);
@@ -312,6 +321,7 @@ public class FileMappingController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@RequestBody LoadParameter newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
this.checkLoadParameter(newEntity);
List<FileMapping> mappings = this.service.listByJob(graphSpace, graph,
jobId);
@@ -327,6 +337,7 @@ public class FileMappingController extends BaseController {
public JobManager nextStep(@PathVariable("graphspace") String graphSpace,
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId) {
+ this.requireGraphSpaceWrite(graphSpace);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
Ex.check(jobEntity.getJobStatus() == JobStatus.MAPPING,
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java
index 99c36da84..cbbe7cbc6 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/FileUploadController.java
@@ -87,6 +87,7 @@ public class FileUploadController extends BaseController {
@PathVariable("jobId") int jobId,
@RequestParam("names")
List<String> fileNames) {
+ this.requireGraphSpaceWrite(graphSpace);
Ex.check(this.jobService.get(graphSpace, graph, jobId) != null,
"job-manager.not-exist.id", jobId);
Ex.check(CollectionUtil.allUnique(fileNames),
@@ -126,6 +127,7 @@ public class FileUploadController extends BaseController {
@RequestParam("token") String token,
@RequestParam("total") int total,
@RequestParam("index") int index) {
+ this.requireGraphSpaceWrite(graphSpace);
this.checkTotalAndIndexValid(total, index);
this.checkFileNameValid(fileName);
this.checkFileNameMatchToken(fileName, token);
@@ -253,6 +255,7 @@ public class FileUploadController extends BaseController {
@PathVariable("jobId") int jobId,
@RequestParam("name") String fileName,
@RequestParam("token") String token) {
+ this.requireGraphSpaceWrite(graphSpace);
this.checkFileNameValid(fileName);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
@@ -286,6 +289,7 @@ public class FileUploadController extends BaseController {
public JobManager nextStep(@PathVariable("graphspace") String graphSpace,
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId) {
+ this.requireGraphSpaceWrite(graphSpace);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
Ex.check(jobEntity.getJobStatus() == JobStatus.UPLOADING,
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java
index f3c58a3e5..a8fb2e731 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/JobManagerController.java
@@ -23,6 +23,7 @@ import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.hugegraph.common.Constant;
import org.apache.hugegraph.common.Response;
+import org.apache.hugegraph.controller.BaseController;
import org.apache.hugegraph.entity.enums.JobStatus;
import org.apache.hugegraph.entity.enums.LoadStatus;
import org.apache.hugegraph.entity.load.FileMapping;
@@ -54,7 +55,7 @@ import java.util.List;
@RestController
@RequestMapping(Constant.API_VERSION + "graphspaces/{graphspace}/graphs" +
"/{graph}/job-manager")
-public class JobManagerController {
+public class JobManagerController extends BaseController {
private static final int LIMIT = 500;
@@ -73,6 +74,7 @@ public class JobManagerController {
public JobManager create(@PathVariable("graphspace") String graphSpace,
@PathVariable("graph") String graph,
@RequestBody JobManager entity) {
+ this.requireGraphSpaceWrite(graphSpace);
synchronized (this.service) {
Ex.check(!StringUtils.isEmpty(entity.getJobName()),
"common.param.cannot-be-null-or-empty", "job_name");
@@ -111,6 +113,7 @@ public class JobManagerController {
public void delete(@PathVariable("graphspace") String graphSpace,
@PathVariable("graph") String graph,
@PathVariable("id") int id) {
+ this.requireGraphSpaceWrite(graphSpace);
this.service.deleteJob(graphSpace, graph, id);
}
@@ -155,6 +158,7 @@ public class JobManagerController {
@PathVariable("graph") String graph,
@PathVariable("id") int id,
@RequestBody JobManager newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
Ex.check(!StringUtils.isEmpty(newEntity.getJobName()),
"common.param.cannot-be-null-or-empty", "job_name");
Ex.check(newEntity.getJobName().length() <= 48,
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java
index 66f534c18..5b45e7800 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/load/LoadTaskController.java
@@ -113,6 +113,7 @@ public class LoadTaskController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@RequestBody LoadTask entity) {
+ this.requireGraphSpaceWrite(graphSpace);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
Ex.check(jobEntity.getJobStatus() == JobStatus.SETTING,
@@ -133,6 +134,7 @@ public class LoadTaskController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@PathVariable("id") int id) {
+ this.requireGraphSpaceWrite(graphSpace);
LoadTask task = this.service.get(graphSpace, graph, jobId, id);
if (task == null) {
throw new ExternalException("load.task.not-exist.id", id);
@@ -152,6 +154,7 @@ public class LoadTaskController extends BaseController {
@PathVariable("jobId") int jobId,
@RequestParam("file_mapping_ids")
List<Integer> fileIds) {
+ this.requireGraphSpaceWrite(graphSpace);
GraphConnection connection = new GraphConnection();
connection.setCluster(config.get(HubbleOptions.PD_CLUSTER));
@@ -205,6 +208,7 @@ public class LoadTaskController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@RequestParam("task_id") int taskId) {
+ this.requireGraphSpaceWrite(graphSpace);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null,
@@ -225,6 +229,7 @@ public class LoadTaskController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@RequestParam("task_id") int taskId) {
+ this.requireGraphSpaceWrite(graphSpace);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null,
@@ -245,6 +250,7 @@ public class LoadTaskController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@RequestParam("task_id") int taskId) {
+ this.requireGraphSpaceWrite(graphSpace);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null,
@@ -265,6 +271,7 @@ public class LoadTaskController extends BaseController {
@PathVariable("graph") String graph,
@PathVariable("jobId") int jobId,
@RequestParam("task_id") int taskId) {
+ this.requireGraphSpaceWrite(graphSpace);
JobManager jobEntity = this.jobService.get(graphSpace, graph, jobId);
Ex.check(jobEntity != null, "job-manager.not-exist.id", jobId);
Ex.check(this.service.get(graphSpace, graph, jobId, taskId) != null,
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java
index b7f1fe24c..945297a38 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/ExecuteHistoryController.java
@@ -65,14 +65,19 @@ public class ExecuteHistoryController extends GremlinController {
@PathVariable("graph") String graph,
@PathVariable("id") int id) {
HugeClient client = this.authClient(graphSpace, graph);
- return this.service.get(client, id);
+ ExecuteHistory history = this.service.get(client, id);
+ if (history == null) {
+ throw new ExternalException("execute-history.not-exist.id", id);
+ }
+ return history;
}
@DeleteMapping("{id}")
public ExecuteHistory delete(@PathVariable("graphspace") String graphSpace,
@PathVariable("graph") String graph,
@PathVariable("id") int id) {
- HugeClient client = this.authClient(graphSpace, graph);
+ HugeClient client = this.requireGraphSpaceWrite(graphSpace);
+ client.assignGraph(graphSpace, graph);
ExecuteHistory oldEntity = this.service.get(client, id);
if (oldEntity == null) {
throw new ExternalException("execute-history.not-exist.id", id);
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java
index 8f66c8590..922b131c1 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/query/GremlinCollectionController.java
@@ -105,14 +105,18 @@ public class GremlinCollectionController extends GremlinController {
}
@GetMapping("{id}")
- public GremlinCollection get(@PathVariable("id") int id) {
- return this.service.get(id);
+ public GremlinCollection get(
+ @PathVariable("graphspace") String graphSpace,
+ @PathVariable("graph") String graph,
+ @PathVariable("id") int id) {
+ return this.service.get(graphSpace, graph, id);
}
@PostMapping
public GremlinCollection create(@PathVariable("graphspace") String graphSpace,
@PathVariable("graph") String graph,
@RequestBody GremlinCollection newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
this.checkParamsValid(newEntity, true);
newEntity.setGraphSpace(graphSpace);
newEntity.setGraph(graph);
@@ -128,29 +132,39 @@ public class GremlinCollectionController extends GremlinController {
}
@PutMapping("{id}")
- public GremlinCollection update(@PathVariable("id") int id,
+ public GremlinCollection update(
+ @PathVariable("graphspace") String graphSpace,
+ @PathVariable("graph") String graph,
+ @PathVariable("id") int id,
@RequestBody GremlinCollection newEntity) {
+ this.requireGraphSpaceWrite(graphSpace);
this.checkIdSameAsBody(id, newEntity);
this.checkParamsValid(newEntity, false);
- GremlinCollection oldEntity = this.service.get(id);
+ GremlinCollection oldEntity = this.service.get(graphSpace, graph, id);
if (oldEntity == null) {
throw new ExternalException("gremlin-collection.not-exist.id", id);
}
GremlinCollection entity = this.mergeEntity(oldEntity, newEntity);
+ entity.setGraphSpace(graphSpace);
+ entity.setGraph(graph);
this.checkEntityUnique(entity, false);
- this.service.update(entity);
+ this.service.update(graphSpace, graph, entity);
return entity;
}
@DeleteMapping("{id}")
- public GremlinCollection delete(@PathVariable("id") int id) {
- GremlinCollection oldEntity = this.service.get(id);
+ public GremlinCollection delete(
+ @PathVariable("graphspace") String graphSpace,
+ @PathVariable("graph") String graph,
+ @PathVariable("id") int id) {
+ this.requireGraphSpaceWrite(graphSpace);
+ GremlinCollection oldEntity = this.service.get(graphSpace, graph, id);
if (oldEntity == null) {
throw new ExternalException("gremlin-collection.not-exist.id", id);
}
- this.service.remove(id);
+ this.service.remove(graphSpace, graph, id);
return oldEntity;
}
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 e3a8e2d6a..60fb08a8a 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
@@ -81,10 +81,14 @@ public class GraphSpaceController extends BaseController {
}
HugeClient client = this.authClient(null, null);
- List<String> graphSpaces =
- this.authMode != null && this.authMode.anonymous() ?
- this.graphSpaceService.listAnonymous(client) :
- this.graphSpaceService.listAll(client);
+ List<String> graphSpaces;
+ if (this.authMode != null && this.authMode.anonymous()) {
+ graphSpaces = this.graphSpaceService.listAnonymous(client);
+ } else if (this.userService.isSuperAdmin(client)) {
+ graphSpaces = this.graphSpaceService.listAll(client);
+ } else {
+ graphSpaces = this.graphSpaceService.listAccessible(client);
+ }
return ImmutableMap.of("graphspaces", graphSpaces);
}
@@ -133,10 +137,16 @@ public class GraphSpaceController extends BaseController {
return ImmutableMap.of("auth", false);
}
HugeClient client = this.authClient(null, null);
- boolean isAuth =
- this.authMode != null && this.authMode.anonymous() ?
- this.graphSpaceService.isAuthForAnonymous(client, graphSpace) :
- this.graphSpaceService.isAuth(client, graphSpace);
+ boolean isAuth;
+ if (this.authMode != null && this.authMode.anonymous()) {
+ isAuth = this.graphSpaceService.isAuthForAnonymous(client,
+ graphSpace);
+ } else if (this.userService.isSuperAdmin(client)) {
+ isAuth = this.graphSpaceService.isAuth(client, graphSpace);
+ } else {
+ isAuth = this.graphSpaceService.isAuthForAccessible(client,
+ graphSpace);
+ }
return ImmutableMap.of("auth", isAuth);
}
@@ -153,9 +163,12 @@ public class GraphSpaceController extends BaseController {
if (this.authMode != null && this.authMode.anonymous()) {
return this.graphSpaceService.getAnonymous(client, graphspace);
}
- // Get GraphSpace Info
- return graphSpaceService.toView(
- graphSpaceService.getWithAdmins(client, graphspace));
+ GraphSpaceEntity entity = this.userService.isSuperAdmin(client) ?
+ graphSpaceService.getWithAdmins(client,
+ graphspace) :
+ graphSpaceService.getAccessibleWithAdmins(
+ client, graphspace);
+ return graphSpaceService.toView(entity);
}
@PostMapping
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/VermeerController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/VermeerController.java
index 27508d836..4206a8fec 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/VermeerController.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/space/VermeerController.java
@@ -67,7 +67,7 @@ public class VermeerController extends BaseController {
String graphspace = body.graphspace;
String graph = body.graph;
String vGraph = vermeerService.convert2VG(graphspace, graph);
- HugeClient client = this.authClient(null, null);
+ HugeClient client = this.authClient(graphspace, graph);
Map<String, Object> graphInfo = HubbleUtil.uncheckedCast(
client.vermeer().getGraphInfoByName(vGraph).get("graph"));
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 09e88c3ec..ba7abc40c 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
@@ -29,6 +29,7 @@ import javax.servlet.http.HttpSession;
//import org.apache.hugegraph.license.LicenseVerifier; // TODO C Remove Licence
import org.apache.hugegraph.service.HugeClientPoolService;
import org.apache.hugegraph.service.auth.AuthModeService;
+import org.apache.hugegraph.service.space.GraphSpaceService;
//import org.apache.hugegraph.service.license.LicenseService;// TODO C Remove Licence
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -36,8 +37,10 @@ import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.apache.hugegraph.common.Constant;
+import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.driver.HugeClient;
import org.apache.hugegraph.exception.ExternalException;
+import org.apache.hugegraph.options.HubbleOptions;
import org.apache.hugegraph.util.PageUtil;
import lombok.extern.log4j.Log4j2;
@@ -52,6 +55,10 @@ public class CustomInterceptor extends HandlerInterceptorAdapter {
protected HugeClientPoolService hugeClientPoolService;
@Autowired
protected AuthModeService authMode;
+ @Autowired
+ protected HugeConfig config;
+ @Autowired
+ protected GraphSpaceService graphSpaceService;
private static final Pattern CHECK_API_PATTERN =
Pattern.compile(".*/graph-connections/\\d+/.+");
@@ -134,7 +141,9 @@ public class CustomInterceptor extends HandlerInterceptorAdapter {
String[] scope = this.requestScope(uri);
String graphSpace = scope[0];
String graph = scope[1];
- if (this.authMode != null && this.authMode.anonymous()) {
+ boolean anonymous = this.authMode != null &&
+ this.authMode.anonymous();
+ if (anonymous) {
client = unauthClient(graphSpace, graph);
} else if (!this.hasAuthSession(request)) {
return;
@@ -143,11 +152,35 @@ public class CustomInterceptor extends HandlerInterceptorAdapter {
(String) request.getSession().getAttribute(Constant.TOKEN_KEY);
client = this.authClient(graphSpace, graph, token);
}
+ this.requireGraphSpaceAccess(client, graphSpace, anonymous);
+ request.setAttribute(Constant.GRAPHSPACE_ACCESS_KEY, graphSpace);
}
request.setAttribute("hugeClient", client);
}
+ protected void requireGraphSpaceAccess(HugeClient client,
+ String graphSpace,
+ boolean anonymous) {
+ if (graphSpace == null || this.config == null ||
+ !this.config.get(HubbleOptions.PD_ENABLED)) {
+ return;
+ }
+ try {
+ if (anonymous) {
+ this.graphSpaceService.requirePublicSpace(client, graphSpace);
+ } else {
+ this.graphSpaceService.requireAccessibleSpace(client,
+ graphSpace);
+ }
+ } catch (RuntimeException e) {
+ if (client != null) {
+ client.close();
+ }
+ throw e;
+ }
+ }
+
private boolean isLoginRequest(String uri) {
return (Constant.API_VERSION + "auth/login").equals(uri) ||
uri.endsWith("/auth/login");
@@ -192,8 +225,13 @@ public class CustomInterceptor extends HandlerInterceptorAdapter {
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);
+ String candidate = parts[i + 1];
+ boolean collectionAction = i + 1 == parts.length - 1 &&
+ ("list".equals(candidate) ||
+ "builtin".equals(candidate));
+ if (!collectionAction) {
+ graphSpace = decodeSegment(candidate);
+ }
}
if ("graphs".equals(parts[i]) && i < parts.length - 1) {
String candidate = parts[i + 1];
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 ae3baee1e..74b62e38b 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
@@ -90,7 +90,6 @@ public class LoginInterceptor extends HandlerInterceptorAdapter {
return false;
}
String scopedAuth = apiPath.substring(0, graphSpaceEnd) + "/auth";
- return apiPath.equals(scopedAuth) ||
- apiPath.startsWith(scopedAuth + "/");
+ return apiPath.startsWith(scopedAuth + "/");
}
}
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 325b6e382..93445da82 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
@@ -470,7 +470,8 @@ public class GraphSpaceUserService extends AuthService {
public boolean hasGraphSpaceAccess(HugeClient client, String graphSpace,
String username) {
if (!client.supportsDefaultRole()) {
- return false;
+ return client.auth().listSpaceMember(graphSpace)
+ .contains(username);
}
return client.graphSpace().checkDefaultRole(
graphSpace, username, "analyst") ||
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 0d44836db..70b114e42 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
@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -235,15 +236,8 @@ public class UserService extends AuthService {
}
User newUser = client.auth().createUser(user);
- List<String> attemptedAdminSpaces = new ArrayList<>();
boolean superAdminAttempted = false;
try {
- if (!permissionPresets && ue.getAdminSpaces() != null) {
- for (String graphspace : ue.getAdminSpaces()) {
- attemptedAdminSpaces.add(graphspace);
- client.auth().addSpaceAdmin(ue.getName(), graphspace);
- }
- }
if (permissionPresets) {
this.graphSpaceUserService
.applyPermissionPresetsForNewAccount(
@@ -251,13 +245,12 @@ public class UserService extends AuthService {
ue.getGraphspacePermissions(),
ue.getPermissionPreset());
}
- if (newUser != null && ue.isSuperadmin()) {
+ if (permissionPresets && newUser != null && ue.isSuperadmin()) {
superAdminAttempted = true;
client.auth().addSuperAdmin(ue.getName());
}
} catch (RuntimeException error) {
this.rollbackNewAccount(client, newUser, ue.getName(),
- attemptedAdminSpaces,
superAdminAttempted, error);
throw error;
}
@@ -265,19 +258,12 @@ public class UserService extends AuthService {
private void rollbackNewAccount(HugeClient client, User user,
String username,
- List<String> attemptedAdminSpaces,
boolean superAdminAttempted,
RuntimeException failure) {
if (superAdminAttempted) {
this.suppressRollback(
() -> client.auth().delSuperAdmin(username), failure);
}
- for (int i = attemptedAdminSpaces.size() - 1; i >= 0; i--) {
- String graphSpace = attemptedAdminSpaces.get(i);
- this.suppressRollback(
- () -> client.auth().delSpaceAdmin(username, graphSpace),
- failure);
- }
if (user != null) {
this.suppressRollback(
() -> client.auth().deleteUser(user.id()), failure);
@@ -590,6 +576,9 @@ public class UserService extends AuthService {
boolean permissionMutation = userEntity.getPermissionPreset() != null;
this.validatePermissionMutation(hugeClient, userEntity, false,
permissionPresets);
+ if (isPdEnabled() && !permissionPresets) {
+ this.validateLegacyPermissionUpdate(hugeClient, userEntity);
+ }
User user = new User();
user.setId(userEntity.getId());
user.name(userEntity.getName());
@@ -604,23 +593,30 @@ public class UserService extends AuthService {
this.updateModernPermissionAccount(hugeClient, user, userEntity);
return;
}
- if (!permissionPresets) {
- updateAdminSpace(hugeClient, userEntity.getName(),
- userEntity.getAdminSpaces());
+ hugeClient.auth().updateUser(user);
+ }
+
+ private void validateLegacyPermissionUpdate(HugeClient client,
+ UserEntity user) {
+ if (user.getPermissionPreset() != null ||
+ user.getGraphspacePermissions() != null &&
+ !user.getGraphspacePermissions().isEmpty()) {
+ throw new ParameterizedException(
+ "auth.permission-preset.unsupported");
}
- if (!permissionPresets) {
- String username = userEntity.getName();
- boolean currentSuperAdmin =
- isSuperAdmin(hugeClient, user.id().toString());
- if (currentSuperAdmin && !userEntity.isSuperadmin()) {
- hugeClient.auth().delSuperAdmin(username);
- }
- if (!currentSuperAdmin && userEntity.isSuperadmin()) {
- hugeClient.auth().addSuperAdmin(username);
+ if (user.getAdminSpaces() != null) {
+ List<String> current = this.listAdminSpace(client, user.getName());
+ if (!new HashSet<>(current).equals(
+ new HashSet<>(user.getAdminSpaces()))) {
+ throw new ParameterizedException(
+ "auth.permission-preset.unsupported");
}
}
-
- hugeClient.auth().updateUser(user);
+ if (user.hasSuperadmin() &&
+ this.isSuperAdmin(client, user.getId()) != user.isSuperadmin()) {
+ throw new ParameterizedException(
+ "auth.permission-preset.unsupported");
+ }
}
private void updateModernPermissionAccount(HugeClient client, User user,
@@ -683,6 +679,15 @@ public class UserService extends AuthService {
boolean create,
boolean supported) {
if (!supported) {
+ if (create && (user.isSuperadmin() ||
+ user.getPermissionPreset() != null ||
+ user.getAdminSpaces() != null &&
+ !user.getAdminSpaces().isEmpty() ||
+ user.getGraphspacePermissions() != null &&
+ !user.getGraphspacePermissions().isEmpty())) {
+ throw new ParameterizedException(
+ "auth.permission-preset.unsupported");
+ }
return;
}
String preset = user.getPermissionPreset();
@@ -693,6 +698,11 @@ public class UserService extends AuthService {
}
return;
}
+ if (!create && user.getPassword() != null &&
+ !user.getPassword().isEmpty()) {
+ throw new ParameterizedException(
+ "Update password and permissions separately");
+ }
boolean superAdminPreset = "SUPER_ADMIN".equals(preset);
if (superAdminPreset != user.isSuperadmin()) {
throw new ParameterizedException(
@@ -779,22 +789,13 @@ public class UserService extends AuthService {
if (adminspaces == null || !isPdEnabled()) {
return;
}
+ if (!hugeClient.supportsDefaultRole()) {
+ throw new ParameterizedException(
+ "auth.permission-preset.unsupported");
+ }
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)) {
this.graphSpaceUserService.applySpacePreset(hugeClient, adminspace, account.id().toString(),
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java
index ecbf666ed..e2578aa27 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/load/JobManagerService.java
@@ -19,6 +19,8 @@
package org.apache.hugegraph.service.load;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -119,6 +121,38 @@ public class JobManagerService {
return this.mapper.selectList(null);
}
+ public List<JobManager> listByGraphSpaces(
+ Collection<String> graphSpaces) {
+ if (graphSpaces != null && graphSpaces.isEmpty()) {
+ return Collections.emptyList();
+ }
+ QueryWrapper<JobManager> query = Wrappers.query();
+ if (graphSpaces != null) {
+ query.in("graphspace", graphSpaces);
+ }
+ query.orderByDesc("create_time");
+ return this.mapper.selectList(query);
+ }
+
+ public IPage<JobManager> listByGraphSpaces(
+ Collection<String> graphSpaces, int pageNo, int pageSize,
+ String content) {
+ Page<JobManager> page = new Page<>(pageNo,
+ PageUtil.boundedSize(pageSize));
+ if (graphSpaces != null && graphSpaces.isEmpty()) {
+ return page;
+ }
+ QueryWrapper<JobManager> query = Wrappers.query();
+ if (graphSpaces != null) {
+ query.in("graphspace", graphSpaces);
+ }
+ if (content != null && !content.isEmpty()) {
+ query.like("job_name", content);
+ }
+ query.orderByDesc("create_time");
+ return this.mapper.selectPage(page, query);
+ }
+
public IPage<JobManager> listAll(int pageNo, int pageSize, String content) {
QueryWrapper<JobManager> query = Wrappers.query();
if (content != null && !content.isEmpty()) {
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java
index a56e85188..1af1a9bbd 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/ExecuteHistoryService.java
@@ -130,7 +130,14 @@ public class ExecuteHistoryService {
}
public ExecuteHistory get(HugeClient client, int id) {
- ExecuteHistory history = this.mapper.selectById(id);
+ QueryWrapper<ExecuteHistory> query = Wrappers.query();
+ query.eq("id", id)
+ .eq("graphspace", client.getGraphSpaceName())
+ .eq("graph", client.getGraphName());
+ ExecuteHistory history = this.mapper.selectOne(query);
+ if (history == null) {
+ return null;
+ }
if (history.getType().equals(ExecuteType.GREMLIN_ASYNC)) {
try {
Task task = client.task().get(history.getAsyncId());
@@ -160,11 +167,18 @@ public class ExecuteHistoryService {
@Transactional(isolation = Isolation.READ_COMMITTED)
public void remove(HugeClient client, int id) {
- ExecuteHistory history = this.mapper.selectById(id);
+ ExecuteHistory history = this.get(client, id);
+ if (history == null) {
+ return;
+ }
if (history.getType().equals(ExecuteType.GREMLIN_ASYNC)) {
client.task().delete(history.getAsyncId());
}
- if (this.mapper.deleteById(id) != 1) {
+ QueryWrapper<ExecuteHistory> query = Wrappers.query();
+ query.eq("id", id)
+ .eq("graphspace", client.getGraphSpaceName())
+ .eq("graph", client.getGraphName());
+ if (this.mapper.delete(query) != 1) {
throw new InternalException("entity.delete.failed", history);
}
}
diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java
index 963c77e5e..a6b016679 100644
--- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java
+++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/query/GremlinCollectionService.java
@@ -111,8 +111,12 @@ public class GremlinCollectionService {
}
}
- public GremlinCollection get(int id) {
- return this.mapper.selectById(id);
+ public GremlinCollection get(String graphSpace, String graph, int id) {
+ QueryWrapper<GremlinCollection> query = Wrappers.query();
+ query.eq("id", id)
+ .eq("graphspace", graphSpace)
+ .eq("graph", graph);
+ return this.mapper.selectOne(query);
}
public GremlinCollection getByName(String graphSpace, String graph,
@@ -137,15 +141,24 @@ public class GremlinCollectionService {
}
@Transactional(isolation = Isolation.READ_COMMITTED)
- public void update(GremlinCollection collection) {
- if (this.mapper.updateById(collection) != 1) {
+ public void update(String graphSpace, String graph,
+ GremlinCollection collection) {
+ QueryWrapper<GremlinCollection> query = Wrappers.query();
+ query.eq("id", collection.getId())
+ .eq("graphspace", graphSpace)
+ .eq("graph", graph);
+ if (this.mapper.update(collection, query) != 1) {
throw new InternalException("entity.update.failed", collection);
}
}
@Transactional(isolation = Isolation.READ_COMMITTED)
- public void remove(int id) {
- if (this.mapper.deleteById(id) != 1) {
+ public void remove(String graphSpace, String graph, int id) {
+ QueryWrapper<GremlinCollection> query = Wrappers.query();
+ query.eq("id", id)
+ .eq("graphspace", graphSpace)
+ .eq("graph", graph);
+ if (this.mapper.delete(query) != 1) {
throw new InternalException("entity.delete.failed", id);
}
}
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 2b5afc2dc..498c56541 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
@@ -186,9 +186,7 @@ public class GraphSpaceService {
space.getNickname().contains(prefix)))
.filter(space -> space.getCreateTime() == null ||
space.getCreateTime().compareTo(after) > 0)
- .filter(space -> !space.isAuth() ||
- client.auth().isSpaceAdmin(space.getName()) ||
- hasCurrentUserAccess(client, space.getName()))
+ .filter(space -> canCurrentUserAccess(client, space))
.collect(Collectors.toList());
Collections.sort(results, (a, b) -> new BuiltInFirst().compare(
a.getName(), b.getName()));
@@ -216,6 +214,14 @@ public class GraphSpaceService {
return client.auth().checkDefaultRole(graphSpace, "observer");
}
+ private static boolean canCurrentUserAccess(HugeClient client,
+ GraphSpace graphSpace) {
+ return !graphSpace.isAuth() ||
+ client.auth().isSuperAdmin() ||
+ client.auth().isSpaceAdmin(graphSpace.getName()) ||
+ hasCurrentUserAccess(client, graphSpace.getName());
+ }
+
public List<Map<String, Object>> queryAnonymousGs(HugeClient client,
String query,
String createTime) {
@@ -230,13 +236,19 @@ public class GraphSpaceService {
.collect(Collectors.toList());
}
+ public List<String> listAccessible(HugeClient client) {
+ return queryAccessibleSpaces(client, "", "").stream()
+ .map(GraphSpace::getName)
+ .collect(Collectors.toList());
+ }
+
public Map<String, Object> getAnonymous(HugeClient client,
String graphSpace) {
- return anonymousView(client, publicSpace(client, graphSpace));
+ return anonymousView(client, requirePublicSpace(client, graphSpace));
}
public boolean isAuthForAnonymous(HugeClient client, String graphSpace) {
- return publicSpace(client, graphSpace).isAuth();
+ return requirePublicSpace(client, graphSpace).isAuth();
}
public IPage<Map<String, Object>> queryAnonymousGsPage(
@@ -300,8 +312,17 @@ public class GraphSpaceService {
return info;
}
- private static GraphSpace publicSpace(HugeClient client,
- String graphSpace) {
+ public GraphSpace requirePublicSpace(HugeClient client,
+ String graphSpace) {
+ GraphSpace space = graphSpaceOrUnavailable(client, graphSpace);
+ if (space.isAuth()) {
+ throw unavailableGraphSpace();
+ }
+ return space;
+ }
+
+ private static GraphSpace graphSpaceOrUnavailable(HugeClient client,
+ String graphSpace) {
GraphSpace space;
try {
space = client.graphSpace().getGraphSpace(graphSpace);
@@ -312,7 +333,7 @@ public class GraphSpaceService {
}
throw e;
}
- if (space == null || space.isAuth()) {
+ if (space == null) {
throw unavailableGraphSpace();
}
return space;
@@ -464,6 +485,11 @@ public class GraphSpaceService {
return space.isAuth();
}
+ public boolean isAuthForAccessible(HugeClient client,
+ String graphSpace) {
+ return requireAccessibleSpace(client, graphSpace).isAuth();
+ }
+
public List<String> listAll(HugeClient client) {
List<String> result = client.graphSpace().listGraphSpace().stream()
.collect(Collectors.toList());
@@ -488,17 +514,34 @@ public class GraphSpaceService {
throw new InternalException("graphspace.get.{} Not Exits",
graphspace);
}
+ return this.withAdmins(authClient, space);
+ }
- GraphSpaceEntity graphSpaceEntity
- = GraphSpaceEntity.fromGraphSpace(space);
+ public GraphSpaceEntity getAccessibleWithAdmins(HugeClient client,
+ String graphSpace) {
+ return this.withAdmins(client, requireAccessibleSpace(client,
+ graphSpace));
+ }
- if (authClient.auth().isSuperAdmin()) {
- graphSpaceEntity.graphspaceAdmin =
- userService.listGraphSpaceAdmin(authClient, graphspace);
+ private GraphSpaceEntity withAdmins(HugeClient client,
+ GraphSpace graphSpace) {
+ GraphSpaceEntity entity = GraphSpaceEntity.fromGraphSpace(graphSpace);
+ if (client.auth().isSuperAdmin()) {
+ String name = graphSpace.getName();
+ entity.graphspaceAdmin =
+ userService.listGraphSpaceAdmin(client, name);
}
- graphSpaceEntity.setStatistic(evCount(authClient, graphspace));
+ entity.setStatistic(evCount(client, graphSpace.getName()));
+ return entity;
+ }
- return graphSpaceEntity;
+ public GraphSpace requireAccessibleSpace(HugeClient client,
+ String graphSpace) {
+ GraphSpace space = graphSpaceOrUnavailable(client, graphSpace);
+ if (!canCurrentUserAccess(client, space)) {
+ throw unavailableGraphSpace();
+ }
+ return space;
}
public void delete(HugeClient authClient, String graphspace) {
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
index 639bc341b..e1c9841db 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/ingest/IngestControllerTest.java
@@ -51,10 +51,12 @@ import org.apache.hugegraph.entity.load.JobManager;
import org.apache.hugegraph.entity.load.LoadTask;
import org.apache.hugegraph.exception.ExternalException;
import org.apache.hugegraph.options.HubbleOptions;
+import org.apache.hugegraph.service.auth.AuthModeService;
import org.apache.hugegraph.service.load.DatasourceService;
import org.apache.hugegraph.service.load.FileMappingService;
import org.apache.hugegraph.service.load.JobManagerService;
import org.apache.hugegraph.service.load.LoadTaskService;
+import org.apache.hugegraph.service.space.GraphSpaceService;
import org.apache.hugegraph.testutil.Assert;
public class IngestControllerTest {
@@ -335,7 +337,15 @@ public class IngestControllerTest {
throws Exception {
TestIngestController controller = new TestIngestController();
LoadTaskService loadTaskService = Mockito.mock(LoadTaskService.class);
+ JobManagerService jobManagerService =
+ Mockito.mock(JobManagerService.class);
this.setField(controller, "loadTaskService", loadTaskService);
+ this.setField(controller, "jobManagerService", jobManagerService);
+ Mockito.when(jobManagerService.get(7))
+ .thenReturn(JobManager.builder()
+ .graphSpace("DEFAULT")
+ .graph("hugegraph")
+ .build());
LoadTask task = LoadTask.builder()
.id(9)
@@ -362,6 +372,99 @@ public class IngestControllerTest {
Assert.assertEquals(63L, metrics.totalTime);
}
+ @Test
+ public void testTaskListFiltersProtectedGraphSpacesInAnonymousMode()
+ throws Exception {
+ TestIngestController controller = new TestIngestController();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.AUTH_ENABLED))
+ .thenReturn(false);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED))
+ .thenReturn(true);
+ GraphSpaceService graphSpaces = Mockito.mock(GraphSpaceService.class);
+ JobManagerService jobs = Mockito.mock(JobManagerService.class);
+ LoadTaskService loadTasks = Mockito.mock(LoadTaskService.class);
+ JobManager visible = JobManager.builder()
+ .id(1)
+ .jobName("visible")
+ .graphSpace("public")
+ .graph("graph")
+ .jobStatus(JobStatus.DEFAULT)
+ .build();
+ com.baomidou.mybatisplus.extension.plugins.pagination.Page<
+ JobManager> visiblePage =
+ new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(
+ 1, 10, 1);
+ visiblePage.setRecords(Collections.singletonList(visible));
+ Mockito.when(jobs.listByGraphSpaces(
+ Collections.singleton("public"), 1, 10, ""))
+ .thenReturn(visiblePage);
+ Mockito.when(graphSpaces.listAnonymous(Mockito.any()))
+ .thenReturn(Collections.singletonList("public"));
+ this.setField(controller, "config", config);
+ this.setField(controller, "authMode", new AuthModeService(config));
+ this.setField(controller, "graphSpaceAccessService", graphSpaces);
+ this.setField(controller, "jobManagerService", jobs);
+ this.setField(controller, "loadTaskService", loadTasks);
+
+ Response response = controller.taskList("", 1, 10);
+
+ @SuppressWarnings("unchecked")
+ com.baomidou.mybatisplus.core.metadata.IPage<
+ IngestController.TaskVO> page =
+ (com.baomidou.mybatisplus.core.metadata.IPage<
+ IngestController.TaskVO>)
+ response.getData();
+ Assert.assertEquals(1L, page.getTotal());
+ @SuppressWarnings("unchecked")
+ Map<String, Object> option = (Map<String, Object>)
+ page.getRecords().get(0).ingestionOption;
+ Assert.assertEquals("public", option.get("graphspace"));
+ Mockito.verify(loadTasks).taskListByJob(1);
+ Mockito.verify(jobs).listByGraphSpaces(
+ Collections.singleton("public"), 1, 10, "");
+ }
+
+ @Test
+ public void testTaskDetailValidatesOwningGraphSpace() throws Exception {
+ TestIngestController controller = new TestIngestController();
+ JobManagerService jobs = Mockito.mock(JobManagerService.class);
+ JobManager job = JobManager.builder()
+ .id(7)
+ .graphSpace("protected")
+ .graph("graph")
+ .build();
+ Mockito.when(jobs.get(7)).thenReturn(job);
+ this.setField(controller, "jobManagerService", jobs);
+
+ Response response = controller.taskDetail(7);
+
+ Assert.assertEquals(Constant.STATUS_OK, response.getStatus());
+ Assert.assertEquals("protected", controller.checkedGraphSpace);
+ }
+
+ @Test
+ public void testJobDetailValidatesParentGraphSpace() throws Exception {
+ TestIngestController controller = new TestIngestController();
+ JobManagerService jobs = Mockito.mock(JobManagerService.class);
+ LoadTaskService loadTasks = Mockito.mock(LoadTaskService.class);
+ LoadTask task = LoadTask.builder().id(9).jobId(7).build();
+ Mockito.when(loadTasks.get(9)).thenReturn(task);
+ Mockito.when(jobs.get(7))
+ .thenReturn(JobManager.builder()
+ .id(7)
+ .graphSpace("protected")
+ .graph("graph")
+ .build());
+ this.setField(controller, "jobManagerService", jobs);
+ this.setField(controller, "loadTaskService", loadTasks);
+
+ Response response = controller.jobDetail(9);
+
+ Assert.assertEquals(Constant.STATUS_OK, response.getStatus());
+ Assert.assertEquals("protected", controller.checkedGraphSpace);
+ }
+
private IngestController.IngestTaskRequest request(Path dataFile) {
IngestController.IngestTaskRequest request =
new IngestController.IngestTaskRequest();
@@ -474,9 +577,24 @@ public class IngestControllerTest {
private static class TestIngestController extends IngestController {
+ private String checkedGraphSpace;
+ private String writeGraphSpace;
+
@Override
protected HugeClient authClient(String graphSpace, String graph) {
return Mockito.mock(HugeClient.class);
}
+
+ @Override
+ protected void requireGraphSpaceAccess(HugeClient client,
+ String graphSpace) {
+ this.checkedGraphSpace = graphSpace;
+ }
+
+ @Override
+ protected HugeClient requireGraphSpaceWrite(String graphSpace) {
+ this.writeGraphSpace = graphSpace;
+ return Mockito.mock(HugeClient.class);
+ }
}
}
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java
index 7911be002..8280a659e 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/GraphSpaceControllerTest.java
@@ -96,6 +96,7 @@ public class GraphSpaceControllerTest {
HugeConfig config = Mockito.mock(HugeConfig.class);
Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
controller.config = config;
+ ReflectionTestUtils.setField(controller, "userService", userService);
ReflectionTestUtils.setField(controller, "graphSpaceService", service);
@SuppressWarnings("unchecked")
@@ -152,6 +153,54 @@ public class GraphSpaceControllerTest {
.evCount(client, "public");
}
+ @Test
+ public void testAuthenticatedEndpointsUseOnlyAccessibleGraphSpaces() {
+ HugeClient client = Mockito.mock(HugeClient.class);
+ UserService userService = Mockito.mock(UserService.class);
+ GraphSpaceService graphSpaceService = Mockito.mock(
+ GraphSpaceService.class);
+ TestGraphSpaceController controller =
+ new TestGraphSpaceController(client);
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ Mockito.when(userService.isSuperAdmin(client)).thenReturn(false);
+ Mockito.when(graphSpaceService.listAccessible(client))
+ .thenReturn(java.util.Collections.singletonList("member"));
+ Mockito.when(graphSpaceService.isAuthForAccessible(client, "member"))
+ .thenReturn(true);
+ GraphSpaceEntity entity = new GraphSpaceEntity();
+ entity.setName("member");
+ Mockito.when(graphSpaceService.getAccessibleWithAdmins(client,
+ "member"))
+ .thenReturn(entity);
+ Mockito.when(graphSpaceService.toView(entity))
+ .thenReturn(java.util.Collections.singletonMap("name",
+ "member"));
+ controller.config = config;
+ ReflectionTestUtils.setField(controller, "userService", userService);
+ ReflectionTestUtils.setField(controller, "graphSpaceService",
+ graphSpaceService);
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> names = (Map<String, Object>) controller.list();
+ @SuppressWarnings("unchecked")
+ Map<String, Object> detail =
+ (Map<String, Object>) controller.get("member");
+ @SuppressWarnings("unchecked")
+ Map<String, Object> auth =
+ (Map<String, Object>) controller.isAuth("member");
+
+ Assert.assertEquals(java.util.Collections.singletonList("member"),
+ names.get("graphspaces"));
+ Assert.assertEquals("member", detail.get("name"));
+ Assert.assertEquals(true, auth.get("auth"));
+ Mockito.verify(graphSpaceService, Mockito.never()).listAll(client);
+ Mockito.verify(graphSpaceService, Mockito.never())
+ .isAuth(client, "member");
+ Mockito.verify(graphSpaceService, Mockito.never())
+ .getWithAdmins(client, "member");
+ }
+
@Test
public void testApplyDefaultsForOptionalResourceLimits() {
GraphSpaceEntity graphSpace = new GraphSpaceEntity();
@@ -211,6 +260,25 @@ public class GraphSpaceControllerTest {
Assert.assertSame(client, controller.requireGlobalManager());
}
+ @Test
+ public void testAnonymousModeCannotMutateGraphSpaces() throws Exception {
+ HugeClient client = Mockito.mock(HugeClient.class);
+ UserService userService = Mockito.mock(UserService.class);
+ GraphSpaceService graphSpaceService = Mockito.mock(
+ GraphSpaceService.class);
+ TestGraphSpaceController controller = controller(
+ client, userService,
+ graphSpaceService);
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)).thenReturn(false);
+ ReflectionTestUtils.setField(controller, "authMode",
+ new AuthModeService(config));
+
+ assertForbidden(() -> controller.add(new GraphSpaceEntity()));
+
+ Mockito.verifyZeroInteractions(userService, graphSpaceService);
+ }
+
@Test
public void testForbiddenGraphSpaceMutationUsesHttpAndBody403()
throws Exception {
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
index 080150e0a..6feee6e3a 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
@@ -175,13 +175,17 @@ public class GraphSpaceUserServiceTest {
}
@Test
- public void testLegacyAccessDoesNotCallNewDefaultRoleApi() {
+ public void testLegacyAccessUsesMembershipWithoutDefaultRoleApi() {
Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
+ Mockito.when(this.auth.listSpaceMember("team"))
+ .thenReturn(Collections.singletonList("alice"));
+ Assert.assertTrue(this.service.hasGraphSpaceAccess(
+ this.client, "team", "alice"));
Assert.assertFalse(this.service.hasGraphSpaceAccess(
- this.client, "team", "alice"));
+ this.client, "team", "bob"));
Mockito.verifyZeroInteractions(this.graphSpace);
- Mockito.verifyZeroInteractions(this.auth);
+ Mockito.verify(this.auth, Mockito.times(2)).listSpaceMember("team");
}
@Test
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
index 6c051e7ba..166aa67f7 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/space/GraphSpaceServiceTest.java
@@ -152,6 +152,9 @@ public class GraphSpaceServiceTest {
.toList()));
Assert.assertTrue((Boolean) response.get(0).get("authed"));
Assert.assertFalse((Boolean) response.get(0).get("default"));
+ Assert.assertEquals(java.util.Arrays.asList(
+ "admin", "analyst", "observer", "public"),
+ this.service.listAccessible(this.client));
Mockito.verify(auth, Mockito.never())
.checkDefaultRole(Mockito.anyString(), Mockito.eq("observer"),
Mockito.anyString());
@@ -255,6 +258,34 @@ public class GraphSpaceServiceTest {
Mockito.verifyZeroInteractions(this.graphsService);
}
+ @Test
+ public void testAuthenticatedDetailRejectsUnassignedProtectedSpace() {
+ GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
+ AuthManager auth = Mockito.mock(AuthManager.class);
+ GraphSpace protectedSpace = graphSpace("protected", true,
+ "20260712");
+ Mockito.when(this.client.graphSpace()).thenReturn(manager);
+ Mockito.when(this.client.auth()).thenReturn(auth);
+ Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
+ Mockito.when(manager.getGraphSpace("protected"))
+ .thenReturn(protectedSpace);
+
+ try {
+ this.service.getAccessibleWithAdmins(this.client, "protected");
+ Assert.fail("Expected protected GraphSpace to be unavailable");
+ } catch (ExternalException e) {
+ Assert.assertEquals(404, e.status());
+ }
+ try {
+ this.service.isAuthForAccessible(this.client, "protected");
+ Assert.fail("Expected protected GraphSpace auth to be unavailable");
+ } catch (ExternalException e) {
+ Assert.assertEquals(404, e.status());
+ }
+ Mockito.verify(auth, Mockito.times(2)).isSpaceMember("protected");
+ Mockito.verifyZeroInteractions(this.graphsService);
+ }
+
@Test
public void testAnonymousAuthHidesMissingGraphSpaceLikeProtectedSpace() {
GraphSpaceManager manager = Mockito.mock(GraphSpaceManager.class);
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 1f0568c98..34594952d 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
@@ -82,6 +82,7 @@ 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.service.space.GraphSpaceService;
import org.apache.hugegraph.structure.auth.Login;
import org.apache.hugegraph.structure.auth.LoginResult;
@@ -210,6 +211,11 @@ public class AuthSecurityTest {
interceptor.preHandle(scopedUsers,
new MockHttpServletResponse(), null));
+ MockHttpServletRequest scopedStatus = new MockHttpServletRequest(
+ "GET", "/api/v1.3/graphspaces/DEFAULT/auth");
+ Assert.assertTrue(interceptor.preHandle(
+ scopedStatus, new MockHttpServletResponse(), null));
+
MockHttpServletRequest authGraph = new MockHttpServletRequest(
"GET", "/api/v1.3/graphspaces/DEFAULT/graphs/auth/schema");
Assert.assertTrue(interceptor.preHandle(
@@ -233,6 +239,55 @@ public class AuthSecurityTest {
Assert.assertNull(request.getAttribute("hugeClient"));
}
+ @Test
+ public void testAnonymousGraphClientRejectsProtectedGraphSpace() {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ RequestContextHolder.setRequestAttributes(
+ new ServletRequestAttributes(request));
+ TestBaseController controller = new TestBaseController();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)).thenReturn(false);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class);
+ HugeClient client = Mockito.mock(HugeClient.class);
+ Mockito.when(spaces.requirePublicSpace(client, "protected"))
+ .thenThrow(new ExternalException(HttpStatus.NOT_FOUND.value(),
+ "unavailable"));
+ ReflectionTestUtils.setField(controller, "config", config);
+ ReflectionTestUtils.setField(controller, "authMode",
+ new AuthModeService(config));
+ ReflectionTestUtils.setField(controller, "graphSpaceAccessService",
+ spaces);
+
+ try {
+ controller.requireSpace(client, "protected");
+ Assert.fail("Expected protected GraphSpace to be unavailable");
+ } catch (ExternalException e) {
+ Assert.assertEquals(HttpStatus.NOT_FOUND.value(), e.status());
+ }
+ Mockito.verify(spaces).requirePublicSpace(client, "protected");
+ }
+
+ @Test
+ public void testBodyGraphSpaceIsValidatedWhenPathScopeDiffers() {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setAttribute(Constant.GRAPHSPACE_ACCESS_KEY, "path-space");
+ RequestContextHolder.setRequestAttributes(
+ new ServletRequestAttributes(request));
+ TestBaseController controller = new TestBaseController();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class);
+ HugeClient client = Mockito.mock(HugeClient.class);
+ ReflectionTestUtils.setField(controller, "config", config);
+ ReflectionTestUtils.setField(controller, "graphSpaceAccessService",
+ spaces);
+
+ controller.requireSpace(client, "body-space");
+
+ Mockito.verify(spaces).requireAccessibleSpace(client, "body-space");
+ }
+
@Test
public void testOnlyBootstrapConfigBypassesLoginInterceptor() {
InterceptorRegistry registry = new InterceptorRegistry();
@@ -371,6 +426,12 @@ public class AuthSecurityTest {
public void testCustomInterceptorCreatesClientForAuthenticatedApi()
throws Exception {
TestCustomInterceptor interceptor = new TestCustomInterceptor();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED))
+ .thenReturn(true);
+ GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class);
+ ReflectionTestUtils.setField(interceptor, "config", config);
+ ReflectionTestUtils.setField(interceptor, "graphSpaceService", spaces);
MockHttpServletRequest request = new MockHttpServletRequest(
"GET",
"/api/v1.3/graphspaces/space1" +
@@ -387,6 +448,7 @@ public class AuthSecurityTest {
Assert.assertEquals("space1", interceptor.graphSpace);
Assert.assertEquals("graph1", interceptor.graph);
Assert.assertEquals("token", interceptor.token);
+ Mockito.verify(spaces).requireAccessibleSpace(null, "space1");
}
@Test
@@ -408,6 +470,30 @@ public class AuthSecurityTest {
Assert.assertEquals(2, interceptor.authClients);
}
+ @Test
+ public void testCustomInterceptorKeepsGraphSpaceCollectionActionsUnscoped()
+ throws Exception {
+ TestCustomInterceptor interceptor = new TestCustomInterceptor();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED))
+ .thenReturn(true);
+ GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class);
+ ReflectionTestUtils.setField(interceptor, "config", config);
+ ReflectionTestUtils.setField(interceptor, "graphSpaceService", spaces);
+
+ for (String action : new String[]{"list", "builtin"}) {
+ MockHttpServletRequest request = new MockHttpServletRequest(
+ "GET", "/api/v1.3/graphspaces/" + action);
+ request.getSession().setAttribute(Constant.TOKEN_KEY, "token");
+ request.getSession().setAttribute(Constant.USERNAME_KEY, "admin");
+
+ Assert.assertTrue(interceptor.preHandle(
+ request, new MockHttpServletResponse(), null));
+ Assert.assertNull(interceptor.graphSpace);
+ }
+ Mockito.verifyZeroInteractions(spaces);
+ }
+
@Test
public void testCustomInterceptorAllowsGraphNamedLikeCollectionAction()
throws Exception {
@@ -429,8 +515,13 @@ public class AuthSecurityTest {
HugeConfig config = Mockito.mock(HugeConfig.class);
Mockito.when(config.get(HubbleOptions.AUTH_ENABLED))
.thenReturn(false);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED))
+ .thenReturn(true);
AuthModeService mode = new AuthModeService(config);
ReflectionTestUtils.setField(interceptor, "authMode", mode);
+ ReflectionTestUtils.setField(interceptor, "config", config);
+ GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class);
+ ReflectionTestUtils.setField(interceptor, "graphSpaceService", spaces);
MockHttpServletRequest request = new MockHttpServletRequest(
"GET", "/api/v1.3/graphspaces/SPACE/graphs/graph/schema");
@@ -438,6 +529,38 @@ public class AuthSecurityTest {
Assert.assertEquals(1, interceptor.unauthClients);
Assert.assertEquals("SPACE", interceptor.graphSpace);
Assert.assertEquals("graph", interceptor.graph);
+ Mockito.verify(spaces).requirePublicSpace(null, "SPACE");
+ }
+
+ @Test
+ public void testAnonymousPathScopeRejectsProtectedGraphSpace()
+ throws Exception {
+ TestCustomInterceptor interceptor = new TestCustomInterceptor();
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.AUTH_ENABLED))
+ .thenReturn(false);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED))
+ .thenReturn(true);
+ ReflectionTestUtils.setField(interceptor, "authMode",
+ new AuthModeService(config));
+ ReflectionTestUtils.setField(interceptor, "config", config);
+ GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class);
+ Mockito.doThrow(new ExternalException(HttpStatus.NOT_FOUND.value(),
+ "unavailable"))
+ .when(spaces).requirePublicSpace(null, "protected");
+ ReflectionTestUtils.setField(interceptor, "graphSpaceService", spaces);
+ MockHttpServletRequest request = new MockHttpServletRequest(
+ "POST", "/api/v1.3/graphspaces/protected/graphs/graph" +
+ "/job-manager/1/upload-file");
+
+ try {
+ interceptor.preHandle(request, new MockHttpServletResponse(), null);
+ Assert.fail("Expected protected GraphSpace to be unavailable");
+ } catch (ExternalException e) {
+ Assert.assertEquals(HttpStatus.NOT_FOUND.value(), e.status());
+ }
+ Assert.assertEquals(1, interceptor.unauthClients);
+ Mockito.verify(spaces).requirePublicSpace(null, "protected");
}
@Test
@@ -868,6 +991,10 @@ public class AuthSecurityTest {
public void clearAuth() {
this.clearAuthSession();
}
+
+ public void requireSpace(HugeClient client, String graphSpace) {
+ this.requireGraphSpaceAccess(client, graphSpace);
+ }
}
private static class TestLoginController extends LoginController {
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java
new file mode 100644
index 000000000..a60c457d9
--- /dev/null
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java
@@ -0,0 +1,49 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.hugegraph.unit;
+
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.controller.ConfigController;
+import org.apache.hugegraph.options.HubbleOptions;
+
+public class ConfigControllerTest {
+
+ @Test
+ public void testBootstrapConfigDoesNotExposeBackendUrl() {
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(false);
+ Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)).thenReturn(true);
+
+ ConfigController controller = new ConfigController();
+ ReflectionTestUtils.setField(controller, "config", config);
+
+ Map<String, Object> result = controller.getConfig();
+
+ Assert.assertEquals(Map.of("pd_enabled", false,
+ "auth_enabled", true), result);
+ Mockito.verify(config, Mockito.never()).get(HubbleOptions.SERVER_URL);
+ }
+}
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingDeletionTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingDeletionTest.java
index 1656cf852..4c5035473 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingDeletionTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/FileMappingDeletionTest.java
@@ -25,6 +25,7 @@ import org.junit.Test;
import org.mockito.Mockito;
import org.apache.hugegraph.controller.load.FileMappingController;
+import org.apache.hugegraph.driver.HugeClient;
import org.apache.hugegraph.entity.enums.LoadStatus;
import org.apache.hugegraph.entity.load.FileMapping;
import org.apache.hugegraph.entity.load.LoadTask;
@@ -70,7 +71,7 @@ public class FileMappingDeletionTest {
private Fixture fixture(LoadStatus status) throws Exception {
Fixture fixture = new Fixture();
- fixture.controller = new FileMappingController();
+ fixture.controller = new TestFileMappingController();
fixture.mappingService = Mockito.mock(FileMappingService.class);
fixture.jobService = Mockito.mock(JobManagerService.class);
LoadTaskService taskService = Mockito.mock(LoadTaskService.class);
@@ -95,9 +96,27 @@ public class FileMappingDeletionTest {
private static void setField(Object target, String name, Object value)
throws Exception {
- Field field = target.getClass().getDeclaredField(name);
- field.setAccessible(true);
- field.set(target, value);
+ Class<?> type = target.getClass();
+ while (type != null) {
+ try {
+ Field field = type.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(target, value);
+ return;
+ } catch (NoSuchFieldException ignored) {
+ type = type.getSuperclass();
+ }
+ }
+ throw new NoSuchFieldException(name);
+ }
+
+ private static class TestFileMappingController
+ extends FileMappingController {
+
+ @Override
+ protected HugeClient requireGraphSpaceWrite(String graphSpace) {
+ return Mockito.mock(HugeClient.class);
+ }
}
private static final class Fixture {
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java
index d22567261..ca0f4f05e 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java
@@ -31,8 +31,12 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.util.NestedServletException;
import org.apache.hugegraph.controller.graphs.GraphsController;
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.controller.BaseController;
import org.apache.hugegraph.driver.HugeClient;
+import org.apache.hugegraph.entity.graphs.GraphCloneEntity;
import org.apache.hugegraph.exception.ExternalException;
+import org.apache.hugegraph.options.HubbleOptions;
import org.apache.hugegraph.service.graphs.GraphsService;
import org.apache.hugegraph.testutil.Assert;
@@ -54,6 +58,10 @@ public class GraphsControllerCanonicalTest {
GraphsController controller = new GraphsController();
this.graphsService = Mockito.mock(GraphsService.class);
this.setField(controller, "graphsService", this.graphsService);
+ HugeConfig config = Mockito.mock(HugeConfig.class);
+ Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(false);
+ this.setField(controller, "config", config);
+ this.setField(BaseController.class, controller, "config", config);
this.client = Mockito.mock(HugeClient.class);
this.withClient = request -> {
@@ -206,9 +214,44 @@ public class GraphsControllerCanonicalTest {
Mockito.verify(this.graphsService).getDefault(this.client);
}
+ @Test
+ public void testCloneValidatesBodyTargetGraphSpace() throws Exception {
+ ScopeCapturingController controller = new ScopeCapturingController();
+ controller.client = this.client;
+ this.setField(controller, "graphsService", this.graphsService);
+ GraphCloneEntity clone = GraphCloneEntity.builder()
+ .graphSpace("target")
+ .name("copy")
+ .build();
+
+ controller.clone("source", "original", clone);
+
+ Assert.assertEquals("target", controller.checkedGraphSpace);
+ Mockito.verify(this.graphsService).clone(
+ Mockito.eq(this.client),
+ Mockito.argThat(params -> "target".equals(
+ params.get("graphspace"))));
+ }
+
private void setField(Object object, String name, Object value)
throws Exception {
- Field field = GraphsController.class.getDeclaredField(name);
+ Class<?> type = object.getClass();
+ while (type != null) {
+ try {
+ Field field = type.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(object, value);
+ return;
+ } catch (NoSuchFieldException ignored) {
+ type = type.getSuperclass();
+ }
+ }
+ throw new NoSuchFieldException(name);
+ }
+
+ private void setField(Class<?> type, Object object, String name,
+ Object value) throws Exception {
+ Field field = type.getDeclaredField(name);
field.setAccessible(true);
field.set(object, value);
}
@@ -218,6 +261,7 @@ public class GraphsControllerCanonicalTest {
private HugeClient client;
private String graphspace;
private String graph;
+ private String checkedGraphSpace;
@Override
protected HugeClient authClient(String graphspace, String graph) {
@@ -225,5 +269,11 @@ public class GraphsControllerCanonicalTest {
this.graph = graph;
return this.client;
}
+
+ @Override
+ protected void requireGraphSpaceAccess(HugeClient client,
+ String graphSpace) {
+ this.checkedGraphSpace = graphSpace;
+ }
}
}
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java
index e8f6f038f..b55def2bb 100644
--- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java
+++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/LoaderScopeControllerTest.java
@@ -23,6 +23,7 @@ import java.util.Arrays;
import org.apache.hugegraph.controller.load.FileMappingController;
import org.apache.hugegraph.controller.load.JobManagerController;
import org.apache.hugegraph.controller.load.LoadTaskController;
+import org.apache.hugegraph.driver.HugeClient;
import org.apache.hugegraph.entity.load.FileMapping;
import org.apache.hugegraph.entity.load.JobManager;
import org.apache.hugegraph.entity.load.LoadTask;
@@ -39,7 +40,7 @@ public class LoaderScopeControllerTest {
@Test
public void testJobCreateRejectsMissingNameAsParameterError() {
JobManagerService service = Mockito.mock(JobManagerService.class);
- JobManagerController controller = new JobManagerController(service);
+ JobManagerController controller = new TestJobManagerController(service);
try {
controller.create("space-a", "graph-a", JobManager.builder().build());
@@ -54,7 +55,7 @@ public class LoaderScopeControllerTest {
public void testJobCreateNormalizesOptionalNullRemarks() {
JobManagerService service = Mockito.mock(JobManagerService.class);
JobManager entity = JobManager.builder().jobName("task_1").build();
- JobManagerController controller = new JobManagerController(service);
+ JobManagerController controller = new TestJobManagerController(service);
controller.create("space-a", "graph-a", entity);
@@ -85,7 +86,7 @@ public class LoaderScopeControllerTest {
.thenReturn(Collections.singletonList(mapping));
Mockito.when(taskService.taskListByJob(7))
.thenReturn(Collections.emptyList());
- FileMappingController controller = new FileMappingController();
+ FileMappingController controller = new TestFileMappingController();
ReflectionTestUtils.setField(controller, "service", service);
ReflectionTestUtils.setField(controller, "jobService", jobService);
ReflectionTestUtils.setField(controller, "taskService", taskService);
@@ -122,4 +123,26 @@ public class LoaderScopeControllerTest {
Mockito.verify(service).list("space-a", "graph-a", 7,
Arrays.asList(13, 14));
}
+
+ private static class TestJobManagerController
+ extends JobManagerController {
+
+ TestJobManagerController(JobManagerService service) {
+ super(service);
+ }
+
+ @Override
+ protected HugeClient requireGraphSpaceWrite(String graphSpace) {
+ return Mockito.mock(HugeClient.class);
+ }
+ }
+
+ private static class TestFileMappingController
+ extends FileMappingController {
+
+ @Override
+ protected HugeClient requireGraphSpaceWrite(String graphSpace) {
+ return Mockito.mock(HugeClient.class);
+ }
+ }
}
diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java
index a3a796181..720915e25 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
@@ -120,12 +120,15 @@ public class UserServiceCompatibilityTest {
.name("user")
.nickname("display-name")
.build();
+ user.setSuperadmin(true);
this.service.update(this.client, user);
ArgumentCaptor<User> request = ArgumentCaptor.forClass(User.class);
Mockito.verify(this.auth).updateUser(request.capture());
Assert.assertNull(request.getValue().nickname());
+ Mockito.verify(this.auth, Mockito.never())
+ .addSuperAdmin(Mockito.anyString());
}
@Test
@@ -291,6 +294,33 @@ public class UserServiceCompatibilityTest {
.checkDefaultRole(Mockito.anyString(), Mockito.anyString());
}
+ @Test
+ public void testLegacyUserDetailKeepsMembershipWithoutPreset() {
+ Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
+ User account = user("alice");
+ account.setId("user-id");
+ Mockito.when(this.auth.getUser("user-id")).thenReturn(account);
+ Mockito.when(this.auth.listUsers())
+ .thenReturn(Collections.singletonList(account));
+ Mockito.when(this.graphSpace.listGraphSpace())
+ .thenReturn(Collections.singletonList("SPACE"));
+ Mockito.when(this.auth.listSpaceAdmin("SPACE"))
+ .thenReturn(Collections.emptyList());
+ Mockito.when(this.graphSpaceUsers.hasGraphSpaceAccess(
+ this.client, "SPACE", "alice"))
+ .thenReturn(true);
+
+ UserEntity result = this.service.get(this.client, "user-id");
+
+ Assert.assertEquals(Collections.singletonList("SPACE"),
+ result.getResSpaces());
+ Assert.assertEquals(Collections.emptyList(),
+ result.getGraphspacePermissions());
+ Assert.assertEquals("LEGACY_CUSTOM", result.getPermissionPreset());
+ Assert.assertEquals(Collections.emptyList(), result.getAdminSpaces());
+ }
+
@Test
public void testLegacyProfileUpdatePreservesPermissionAssignments() {
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
@@ -369,6 +399,33 @@ public class UserServiceCompatibilityTest {
.addSuperAdmin(Mockito.anyString());
}
+ @Test
+ public void testModernCombinedPasswordAndPermissionUpdateIsRejected() {
+ Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+ UserEntity account = UserEntity.builder()
+ .id("user")
+ .name("user")
+ .password("new-password")
+ .build();
+ account.setPermissionPreset("GS_READ_ONLY");
+ account.setGraphspacePermissions(Collections.singletonList(
+ permission("team", "GS_READ_ONLY")));
+
+ try {
+ this.service.update(this.client, account);
+ Assert.fail("Expected combined update to be rejected");
+ } catch (ParameterizedException ignored) {
+ // Expected
+ }
+
+ Mockito.verify(this.auth, Mockito.never())
+ .updateUser(Mockito.any(User.class));
+ Mockito.verify(this.graphSpaceUsers, Mockito.never())
+ .applyPermissionPresets(Mockito.any(), Mockito.anyString(),
+ Mockito.any(), Mockito.any());
+ }
+
@Test
public void testModernUserUpdateRollsBackProfileAndSuperAdmin() {
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
@@ -411,7 +468,7 @@ public class UserServiceCompatibilityTest {
}
@Test
- public void testLegacyUserUpdateReconcilesAdminSpaces() {
+ public void testLegacyUserUpdateRejectsPermissionChangesBeforeWrites() {
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
Mockito.when(this.graphSpace.listGraphSpace())
@@ -420,11 +477,6 @@ public class UserServiceCompatibilityTest {
.thenReturn(Collections.singletonList("user"));
Mockito.when(this.auth.listSpaceAdmin("NEW"))
.thenReturn(Collections.emptyList());
- Mockito.when(this.client.findUserByName("user"))
- .thenReturn(user("user"));
- Mockito.when(this.auth.listSuperAdmin())
- .thenReturn(Collections.singletonList("user"));
- Mockito.when(this.auth.getUser("user")).thenReturn(user("user"));
UserEntity account = UserEntity.builder()
.id("user")
.name("user")
@@ -432,15 +484,51 @@ public class UserServiceCompatibilityTest {
Collections.singletonList("NEW"))
.build();
- this.service.update(this.client, account);
+ ParameterizedException error = null;
+ try {
+ this.service.update(this.client, account);
+ } catch (ParameterizedException e) {
+ error = e;
+ }
- Mockito.verify(this.auth).addSpaceAdmin("user", "NEW");
- Mockito.verify(this.auth).delSpaceAdmin("user", "OLD");
- Mockito.verify(this.auth).delSuperAdmin("user");
- Mockito.verify(this.graphSpaceUsers, Mockito.never())
- .applySpacePreset(Mockito.any(), Mockito.anyString(),
- Mockito.anyString(), Mockito.anyString(),
- Mockito.anyString());
+ Assert.assertNotNull(error);
+ Assert.assertEquals("auth.permission-preset.unsupported",
+ error.getMessage());
+ Mockito.verify(this.auth, Mockito.never())
+ .addSpaceAdmin(Mockito.anyString(), Mockito.anyString());
+ Mockito.verify(this.auth, Mockito.never())
+ .delSpaceAdmin(Mockito.anyString(), Mockito.anyString());
+ Mockito.verify(this.auth, Mockito.never())
+ .updateUser(Mockito.any(User.class));
+ }
+
+ @Test
+ public void testLegacyUserUpdateRejectsPresetBeforeProfileWrite() {
+ Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
+ UserEntity account = UserEntity.builder()
+ .id("user")
+ .name("user")
+ .permissionPreset("GS_READ_ONLY")
+ .graphspacePermissions(
+ Collections.singletonList(
+ permission(
+ "SPACE",
+ "GS_READ_ONLY")))
+ .build();
+
+ ParameterizedException error = null;
+ try {
+ this.service.update(this.client, account);
+ } catch (ParameterizedException e) {
+ error = e;
+ }
+
+ Assert.assertNotNull(error);
+ Assert.assertEquals("auth.permission-preset.unsupported",
+ error.getMessage());
+ Mockito.verify(this.auth, Mockito.never())
+ .updateUser(Mockito.any(User.class));
}
@Test
@@ -485,7 +573,7 @@ public class UserServiceCompatibilityTest {
}
@Test
- public void testLegacyUserCreationSkipsPermissionPresetApis() {
+ public void testLegacyUserCreationRejectsPermissionPreset() {
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
UserEntity user = userEntity("display-name");
@@ -494,9 +582,18 @@ public class UserServiceCompatibilityTest {
java.util.Collections.singletonMap(
"permission_preset", "GS_READ_ONLY")));
- this.service.add(this.client, user);
+ ParameterizedException error = null;
+ try {
+ this.service.add(this.client, user);
+ } catch (ParameterizedException e) {
+ error = e;
+ }
- Mockito.verify(this.auth).createUser(Mockito.any(User.class));
+ Assert.assertNotNull(error);
+ Assert.assertEquals("auth.permission-preset.unsupported",
+ error.getMessage());
+ Mockito.verify(this.auth, Mockito.never())
+ .createUser(Mockito.any(User.class));
Mockito.verify(this.graphSpaceUsers, Mockito.never())
.validatePermissionPresets(Mockito.any(), Mockito.any(),
Mockito.any());
@@ -587,30 +684,46 @@ public class UserServiceCompatibilityTest {
}
@Test
- public void testLegacyAdminGrantFailureRollsBackEarlierSpaces() {
+ public void testLegacyAdminGrantIsRejectedBeforeAccountCreation() {
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
- User created = user("user");
- created.setId("u-1");
- Mockito.when(this.auth.createUser(Mockito.any(User.class)))
- .thenReturn(created);
- RuntimeException failure = new RuntimeException("grant failed");
- Mockito.when(this.auth.addSpaceAdmin("user", "SECOND"))
- .thenThrow(failure);
UserEntity user = userEntity("display-name");
user.setAdminSpaces(Arrays.asList("FIRST", "SECOND"));
- RuntimeException error = null;
+ ParameterizedException error = null;
try {
this.service.add(this.client, user);
- } catch (RuntimeException e) {
+ } catch (ParameterizedException e) {
error = e;
}
- Assert.assertSame(failure, error);
- Mockito.verify(this.auth).delSpaceAdmin("user", "SECOND");
- Mockito.verify(this.auth).delSpaceAdmin("user", "FIRST");
- Mockito.verify(this.auth).deleteUser("u-1");
+ Assert.assertNotNull(error);
+ Assert.assertEquals("auth.permission-preset.unsupported",
+ error.getMessage());
+ Mockito.verify(this.auth, Mockito.never())
+ .createUser(Mockito.any(User.class));
+ Mockito.verify(this.auth, Mockito.never())
+ .addSpaceAdmin(Mockito.anyString(), Mockito.anyString());
+ }
+
+ @Test
+ public void testLegacyAdminSpaceEndpointRejectsBeforeWrites() {
+ Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+ Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
+
+ ParameterizedException error = null;
+ try {
+ this.service.updateAdminSpace(
+ this.client, "user", Collections.singletonList("SPACE"));
+ } catch (ParameterizedException e) {
+ error = e;
+ }
+
+ Assert.assertNotNull(error);
+ Assert.assertEquals("auth.permission-preset.unsupported",
+ error.getMessage());
+ Mockito.verify(this.auth, Mockito.never())
+ .addSpaceAdmin(Mockito.anyString(), Mockito.anyString());
}
@Test
diff --git a/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js
index febdbdc95..c95d11e38 100644
--- a/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js
+++ b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js
@@ -24,6 +24,7 @@ const loadResponseHandlers = modulePath => {
const modalWarning = jest.fn();
const clearLogin = jest.fn();
const isLogoutTransition = jest.fn(() => false);
+ const isAuthEnabled = jest.fn(() => true);
const instance = {
interceptors: {
request: {
@@ -59,6 +60,9 @@ const loadResponseHandlers = modulePath => {
clearLogin,
isLogoutTransition,
}));
+ jest.doMock('../utils/config', () => ({
+ isAuthEnabled,
+ }));
const request = require(modulePath).default;
return {
@@ -68,6 +72,7 @@ const loadResponseHandlers = modulePath => {
modalWarning,
clearLogin,
isLogoutTransition,
+ isAuthEnabled,
instance,
request,
};
@@ -90,6 +95,7 @@ describe.each(['./request'])('%s error semantics', modulePath => {
jest.dontMock('antd');
jest.dontMock('../i18n');
jest.dontMock('../utils/user');
+ jest.dontMock('../utils/config');
localStorage.clear();
sessionStorage.clear();
});
@@ -239,6 +245,28 @@ describe.each(['./request'])('%s error semantics', modulePath => {
expect(instance.delete).not.toHaveBeenCalled();
});
+ it('keeps anonymous HTTP 401 local and shows the resource error', async () => {
+ const {reject, clearLogin, isAuthEnabled, messageError}
+ = loadResponseHandlers(modulePath);
+ isAuthEnabled.mockReturnValue(false);
+ const error = {
+ config: {url: '/graphspaces/protected/graphs/graph/schema'},
+ response: {
+ status: 401,
+ data: {
+ status: 401,
+ message: 'GraphSpace is unavailable',
+ },
+ },
+ };
+
+ await expect(reject(error)).rejects.toBe(error);
+ expect(clearLogin).not.toHaveBeenCalled();
+ expect(sessionStorage.getItem('redirect')).toBeNull();
+ expect(window.location.href).toBe('');
+ expect(messageError).toHaveBeenCalledWith('request.error');
+ });
+
it('rejects business 401 and redirects to login', async () => {
const {resolve, clearLogin, instance} = loadResponseHandlers(modulePath);
const response = {
diff --git a/hugegraph-hubble/hubble-fe/src/api/request.js b/hugegraph-hubble/hubble-fe/src/api/request.js
index 1b63ae16e..32e0aac98 100644
--- a/hugegraph-hubble/hubble-fe/src/api/request.js
+++ b/hugegraph-hubble/hubble-fe/src/api/request.js
@@ -25,6 +25,7 @@ import * as user from '../utils/user';
import {withLanguageHeader} from './languageHeader';
import {showThrottleWarning} from './throttleWarning';
import {AUTH_REVALIDATE_EVENT} from '../utils/authEvents';
+import {isAuthEnabled} from '../utils/config';
import {sanitizePublicError} from '../utils/publicError';
const isJsonResponse = headers => {
@@ -152,9 +153,12 @@ instance.interceptors.response.use(
if (isLoginRequest(response.config)) {
showLoginAuthError(response);
}
- else {
+ else if (isAuthEnabled()) {
redirectToLogin();
}
+ else {
+ showRequestError(response.data);
+ }
return Promise.reject(response);
}
else if (response.data?.status === 429) {
@@ -184,9 +188,12 @@ instance.interceptors.response.use(
if (isLoginRequest(error.config)) {
showLoginAuthError(error.response);
}
- else {
+ else if (isAuthEnabled()) {
redirectToLogin();
}
+ else {
+ showRequestError(error.response?.data);
+ }
return Promise.reject(error);
}
if (error.response?.status === 429
@@ -219,7 +226,7 @@ const request = {};
const responseData = response => {
const data = response?.data;
- if (data?.status === 401) {
+ if (data?.status === 401 && isAuthEnabled()) {
redirectToLogin();
}
return data;
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 07838bafd..7547d188f 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
@@ -403,6 +403,7 @@
"save_failed": "Account was not saved",
"save_retry": "Check the account fields and server connection, then retry.",
"delete_retry": "The account could not be deleted. Refresh the list and retry.",
+ "password_permission_separate": "Save password and permission changes separately.",
"presets_unavailable": "GraphSpace presets are unavailable",
"presets_unavailable_help": "This deployment does not provide the required permission API. The account will be created without elevated permissions",
"preset_edit_unavailable": "Permission editing is unavailable",
@@ -1343,6 +1344,7 @@
"reason_refresh_failed": "Refresh failed",
"reason_malformed_response": "Malformed response",
"reason_unsupported_version": "Unsupported service version",
+ "reason_unsupported_version_help": "Upgrade HugeGraph to a version that provides this metric.",
"reason_deployment_mode_unsupported": "Unsupported by the current deployment mode",
"metric_labels": {
"basic": "Memory & process",
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 1efe05188..322f3e4ce 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
@@ -403,6 +403,7 @@
"save_failed": "账号未保存",
"save_retry": "请检查账号字段和 Server 连接后重试。",
"delete_retry": "账号删除失败,请刷新列表后重试。",
+ "password_permission_separate": "请分别保存密码和权限变更。",
"presets_unavailable": "GraphSpace 权限预设不可用",
"presets_unavailable_help": "当前部署未提供所需权限 API,账号将以无提升权限的普通账号创建",
"preset_edit_unavailable": "当前无法编辑权限",
@@ -1343,6 +1344,7 @@
"reason_refresh_failed": "刷新失败",
"reason_malformed_response": "响应格式异常",
"reason_unsupported_version": "当前服务版本不支持",
+ "reason_unsupported_version_help": "请升级 HugeGraph 到提供该指标的版本。",
"reason_deployment_mode_unsupported": "当前部署模式不支持",
"metric_labels": {
"basic": "内存与进程",
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index df60813f8..1847ca1fd 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -43,6 +43,9 @@ const toProfilePayload = values => ({
user_password: values.user_password,
user_description: values.user_description,
});
+const sameSpaces = (left = [], right = []) => (
+ [...left].sort().join('\u0000') === [...right].sort().join('\u0000')
+);
const loadAllGraphspaces = () => loadAllPages(api.manage.getGraphSpaceList);
@@ -104,7 +107,17 @@ const EditLayer = ({
});
}, [onCancel, permissionPresetsSupported, refresh, t]);
const updateUser = useCallback(values => {
- const payload = permissionPresetsSupported
+ const initialPreset = getAccountPreset(detail) ?? PRESERVE_PERMISSIONS;
+ const permissionsChanged = permissionPresetsSupported
+ && (values.permission_preset !== initialPreset
+ || !sameSpaces(
+ values.graphspaces,
+ getPresetSpaces(detail)
+ ));
+ if (values.user_password && permissionsChanged) {
+ throw new Error(t('account.feedback.password_permission_separate'));
+ }
+ const payload = permissionsChanged
&& values.permission_preset !== PRESERVE_PERMISSIONS
? toPermissionPayload(values)
: toProfilePayload(values);
@@ -119,7 +132,7 @@ const EditLayer = ({
throw res;
});
- }, [onCancel, refresh, data.id, permissionPresetsSupported, t]);
+ }, [onCancel, refresh, data.id, detail, permissionPresetsSupported, t]);
const updateUserAuth = useCallback(values => {
const payload = toPermissionPayload({
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
index 14c5e20f2..bde43ce88 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js
@@ -210,6 +210,79 @@ test('preserves mixed GraphSpace permissions on profile edit', async () => {
expect(payload).not.toHaveProperty('is_superadmin');
});
+test('updates a password without resubmitting unchanged permissions', async () => {
+ mockAuthContext = {
+ capabilities: ['accounts_manage', 'account_permission_presets'],
+ };
+ api.auth.getUserInfo.mockResolvedValue({
+ status: 200,
+ data: {
+ user_name: 'alice',
+ permission_preset: 'GS_READ_ONLY',
+ graphspace_permissions: [{
+ graphspace: 'SPACE',
+ permission_preset: 'GS_READ_ONLY',
+ }],
+ },
+ });
+ api.auth.updateUser.mockResolvedValue({status: 200});
+
+ render(<EditLayer {...props} data={{id: 'alice'}} op='edit' />);
+
+ await screen.findByDisplayValue('alice');
+ const password = screen.getByPlaceholderText(
+ 'account.form.default_password_placeholder'
+ );
+ fireEvent.change(password, {target: {value: 'new-password'}});
+ fireEvent.click(document.querySelector(
+ '.ant-modal-footer .ant-btn-primary'
+ ));
+
+ await waitFor(() => expect(api.auth.updateUser).toHaveBeenCalled());
+ const payload = api.auth.updateUser.mock.calls[0][1];
+ expect(payload.user_password).toBe('new-password');
+ expect(payload).not.toHaveProperty('permission_preset');
+ expect(payload).not.toHaveProperty('graphspace_permissions');
+});
+
+test('rejects a combined password and permission edit before the request', async () => {
+ mockAuthContext = {
+ capabilities: ['accounts_manage', 'account_permission_presets'],
+ };
+ api.auth.getUserInfo.mockResolvedValue({
+ status: 200,
+ data: {
+ user_name: 'alice',
+ permission_preset: 'GS_READ_ONLY',
+ graphspace_permissions: [{
+ graphspace: 'SPACE',
+ permission_preset: 'GS_READ_ONLY',
+ }],
+ },
+ });
+
+ render(<EditLayer {...props} data={{id: 'alice'}} op='edit' />);
+
+ await screen.findByDisplayValue('alice');
+ const password = screen.getByPlaceholderText(
+ 'account.form.default_password_placeholder'
+ );
+ fireEvent.change(password, {target: {value: 'new-password'}});
+ fireEvent.mouseDown(screen.getAllByRole('combobox')[0]);
+ fireEvent.click(screen.getByText(
+ 'account.permission_preset.GS_READ_WRITE'
+ ));
+ fireEvent.click(document.querySelector(
+ '.ant-modal-footer .ant-btn-primary'
+ ));
+
+ const alert = await screen.findByRole('alert');
+ expect(alert).toHaveTextContent(
+ 'account.feedback.password_permission_separate'
+ );
+ expect(api.auth.updateUser).not.toHaveBeenCalled();
+});
+
test('ignores a second account mutation while the first submit is pending', async () => {
const detailRequest = deferred();
const mutation = deferred();
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js
index 8269d5699..4d133bc5b 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.js
@@ -16,7 +16,7 @@
* under the License.
*/
-import {Alert, Button, Descriptions, Progress, Skeleton, Space, Statistic} from 'antd';
+import {Alert, Button, Descriptions, Progress, Skeleton, Space, Statistic, Tooltip} from 'antd';
import {ArrowLeftOutlined} from '@ant-design/icons';
import {useCallback, useEffect, useRef, useState} from 'react';
import {useLocation, useNavigate, useParams} from 'react-router-dom';
@@ -319,6 +319,13 @@ const MetricGroup = ({group, name, values, status = {}, emptyMessage}) => {
const reasonLabel = status.reason ? t(`operations.reason_${status.reason}`, {
defaultValue: status.reason.replaceAll('_', ' '),
}) : null;
+ const upgradeHelp = status.reason === 'unsupported_version'
+ ? t('operations.reason_unsupported_version_help') : null;
+ const availabilityStatus = (
+ <strong className={`availability-${availability.toLowerCase()}`}>
+ {availabilityLabel}
+ </strong>
+ );
const statusDetails = (
<div
className='operations-metric-status'
@@ -336,9 +343,11 @@ const MetricGroup = ({group, name, values, status = {}, emptyMessage}) => {
<header className='operations-metric-header'>
<div className='operations-metric-title-row'>
<h3>{name}</h3>
- <strong className={`availability-${availability.toLowerCase()}`}>
- {availabilityLabel}
- </strong>
+ {upgradeHelp ? (
+ <Tooltip title={upgradeHelp}>
+ <span tabIndex={0}>{availabilityStatus}</span>
+ </Tooltip>
+ ) : availabilityStatus}
</div>
{statusDetails}
</header>
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js
index f3796c8eb..2d6cbc506 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/NodeDetail.test.js
@@ -340,6 +340,10 @@ test('renders each metric group from its own metric status', async () => {
expect(within(raft).getByText('Unsupported service version')).toBeInTheDocument();
expect(within(raft).getByText(/Unsupported by this service version/))
.toBeInTheDocument();
+ fireEvent.mouseOver(within(raft).getByText('Unsupported'));
+ expect(await screen.findByText(
+ 'Upgrade HugeGraph to a version that provides this metric.'
+ )).toBeInTheDocument();
const backend = screen.getByRole('heading', {name: 'Backend'}).closest('section');
expect(within(backend).getByText('Available')).toBeInTheDocument();