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 9c86974c84f9b2038cb9be6d74aa63ad15de75f4 Author: dark <[email protected]> AuthorDate: Sat Aug 29 23:45:52 2026 +0800 fix(hubble): close final auth and Store gaps - clear account identity when entering anonymous mode - mark deadline-cancelled Stores down - cover storage cleanup and fanout cancellation --- .github/workflows/hubble-ci.yml | 1 + hugegraph-hubble/AGENTS.md | 32 +++-------- .../hugegraph/controller/BaseController.java | 9 ++- .../hugegraph/controller/ConfigController.java | 6 +- .../hugegraph/controller/auth/UserController.java | 5 ++ .../controller/graphs/GraphsController.java | 2 +- .../service/op/DefaultOperationsDataService.java | 6 +- .../service/op/LiveOperationsCollector.java | 59 +++++++++++++++----- .../service/op/OperationsPayloadParser.java | 18 ++++-- .../auth/AccountMutationAuthorizationTest.java | 17 ++++++ .../op/DefaultOperationsDataServiceTest.java | 37 +++++++++++++ .../service/op/LiveOperationsCollectorTest.java | 64 +++++++++++++++++++++- .../service/op/OperationsPayloadParserTest.java | 19 +++++++ .../hugegraph/unit/ConfigControllerTest.java | 4 +- .../unit/GraphsControllerCanonicalTest.java | 4 +- hugegraph-hubble/hubble-fe/src/App.js | 13 +++++ hugegraph-hubble/hubble-fe/src/App.test.js | 4 ++ .../hubble-fe/src/pages/Account/EditLayer.js | 34 ++++++++---- .../pages/Account/account-edit-recovery.test.js | 42 ++++++++++++++ .../src/pages/Account/account-recovery.test.js | 28 +++++++++- .../hubble-fe/src/pages/Account/index.js | 4 +- 21 files changed, 338 insertions(+), 70 deletions(-) diff --git a/.github/workflows/hubble-ci.yml b/.github/workflows/hubble-ci.yml index db2ec0ba2..8a753b01e 100644 --- a/.github/workflows/hubble-ci.yml +++ b/.github/workflows/hubble-ci.yml @@ -112,6 +112,7 @@ jobs: yarn install --frozen-lockfile --network-timeout 600000 --prefer-offline --ignore-engines yarn lint yarn i18n:check + yarn test --watchAll=false --runInBand node --test ../hubble-dist/assembly/travis/ui_auth.test.js - name: Install Playwright Chromium diff --git a/hugegraph-hubble/AGENTS.md b/hugegraph-hubble/AGENTS.md index 4793edafc..e47b828c2 100644 --- a/hugegraph-hubble/AGENTS.md +++ b/hugegraph-hubble/AGENTS.md @@ -2,38 +2,20 @@ ## Authentication and connection boundary -The `1.8/master` path is the source of truth. The backend detects authentication -mode from HugeGraph Server and uses one connection resolver. The resolver -chooses either a direct server URL or an address discovered from PD; callers -must not reimplement `usePD` or infer connection state from page-local flags. -In PD mode the server address returned by discovery is authoritative, so a -manual server URL is not required. - -Use the unauthenticated HugeGraph client for anonymous mode. Do not manufacture -an empty token or an administrator session. Anonymous mode has no account -context and account/permission routes are hidden or rejected at the capability -boundary. +The `1.8/master` path is the source of truth. The backend detects authentication mode from HugeGraph Server and uses one connection resolver. The resolver chooses either a direct server URL or an address discovered from PD; callers must not reimplement `usePD` or infer connection state from page-local flags. In PD mode the server address returned by discovery is authoritative, so a manual server URL is not required. + +Use the unauthenticated HugeGraph client for anonymous mode. Do not manufacture an empty token or an administrator session. Anonymous mode has no account context and account/permission routes are hidden or rejected at the capability boundary. ## Compatibility policy Compatibility is intentionally one-way: - `1.8/master`: modern GraphSpace/auth contracts and the complete UI. -- `1.7`: thin fallback for the legacy response shape; keep the core workflow - usable without adding version branches to controllers or React pages. -- `1.5` standalone: core graph/schema/data operations only. GraphSpace - management is unsupported and should degrade with an explicit capability - response. Do not add a PD variant for 1.5. +- `1.7`: thin fallback for the legacy response shape; keep the core workflow usable without adding version branches to controllers or React pages. +- `1.5` standalone: core graph/schema/data operations only. GraphSpace management is unsupported and should degrade with an explicit capability response. Do not add a PD variant for 1.5. -Version checks belong in the client compatibility adapter and connection -resolver. New code should consume capabilities, not compare literal versions. -When an old image cannot satisfy a capability, mark the test as `needs input` -or `skipped` with the exact image tag and reason. +Version checks belong in the client compatibility adapter and connection resolver. New code should consume capabilities, not compare literal versions. When an old image cannot satisfy a capability, mark the test as `needs input` or `skipped` with the exact image tag and reason. ## Verification -For UI changes, use Chrome to exercise login/non-auth mode, connection -switching, and account/GraphSpace visibility. Static inspection and unit tests -are not a substitute for this interaction check. Keep screenshots collected -from the running UI in the documentation assets referenced by -`README.md`. +For UI changes, use Chrome to exercise login/non-auth mode, connection switching, and account/GraphSpace visibility. Static inspection and unit tests are not a substitute for this interaction check. Keep screenshots collected from the running UI in the documentation assets referenced by `README.md`. 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 a410c7d55..eec2c0e70 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 @@ -191,11 +191,16 @@ public abstract class BaseController { protected HugeClient requireGraphSpaceWrite(String graphSpace) { HugeClient client = this.authClient(null, null); + this.requireGraphSpaceWrite(client, graphSpace); + client.assignGraph(graphSpace, null); + return client; + } + + protected void requireGraphSpaceWrite(HugeClient client, + String graphSpace) { this.requireGraphSpaceAccess(client, graphSpace); this.authContextService.requireGraphSpaceWrite( client, this.getUser(), graphSpace); - client.assignGraph(graphSpace, null); - return client; } protected HugeClient requireGraphSpaceAuthorizationAdmin( 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 6c212acfe..2793fbb6a 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 @@ -61,8 +61,8 @@ public class ConfigController { if (pdEnabled) { capabilities.put("server_capabilities_verified", false); capabilities.put("auth_enabled", true); - capabilities.put("graph_create_enabled", true); - capabilities.put("cypher_enabled", true); + capabilities.put("graph_create_enabled", false); + capabilities.put("cypher_enabled", false); if (hugeClientPoolService == null) { return capabilities; } @@ -70,6 +70,8 @@ public class ConfigController { capabilities.put("auth_enabled", this.authModeService.update( client.isServerAuthEnabled())); + capabilities.put("graph_create_enabled", true); + capabilities.put("cypher_enabled", true); capabilities.put("server_capabilities_verified", true); return capabilities; } catch (RuntimeException ignored) { diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java index fd4d7b1b7..0c5aa4cd1 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java @@ -115,6 +115,11 @@ public class UserController extends BaseController { } this.checkAccountGrantScope(client, current.getName(), userEntity); userService.update(client, userEntity); + if (Objects.equals(this.getUser(), current.getName()) && + userEntity.getPassword() != null && + !userEntity.getPassword().isEmpty()) { + this.clearAuthSession(); + } } @DeleteMapping("{id}") 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 8e2bef18f..0554ba625 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 @@ -337,7 +337,7 @@ public class GraphsController extends BaseController { String targetGraphSpace = graphCloneEntity.getGraphSpace() == null ? graphspace : graphCloneEntity.getGraphSpace(); - this.requireGraphSpaceAccess(client, targetGraphSpace); + this.requireGraphSpaceWrite(client, targetGraphSpace); return this.graphsService.clone( client, graphCloneEntity.convertMap(graphspace, graph)); } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java index b95fca4db..180394eb0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java @@ -51,10 +51,10 @@ import org.apache.hugegraph.service.op.OperationsModels.SourceStatus; public class DefaultOperationsDataService implements OperationsDataService { private static final Set<String> PD_FACTS = Set.of( - "graphs", "partitions", "replicas", "stores", "stores_up", - "data_size_bytes"); + "graphs", "partitions", "replicas", "data_size_bytes"); private static final Set<String> STORE_FACTS = Set.of( - "capacity_total_bytes", "capacity_used_bytes"); + "stores", "stores_up", "capacity_total_bytes", + "capacity_used_bytes"); private final OperationsCollector collector; private final long ttlMillis; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java index 5d2dd9dc4..a633f9fe7 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java @@ -38,6 +38,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -210,7 +211,14 @@ public class LiveOperationsCollector implements OperationsCollector { private void collectServer(HugeClient client, boolean includeMetrics, long now, Map<String, SourceStatus> sources, List<Node> nodes) { - List<String> urls = this.discoveredServerURLs(); + long started = System.nanoTime(); + List<String> urls; + try { + urls = this.discoveredServerURLs(); + } catch (RuntimeException e) { + sources.put("server", unavailable(metricReason(e), now)); + return; + } if (urls.isEmpty()) { this.collectSingleServer(client, this.serverIdentity, "HugeGraph Server", includeMetrics, now, @@ -222,6 +230,10 @@ public class LiveOperationsCollector implements OperationsCollector { String reason = null; String authContext = client.getAuthContext(); List<Future<ServerResult>> futures; + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis( + System.nanoTime() - started); + long remainingMillis = Math.max( + 1L, this.storeDeadlineMillis - elapsedMillis); try { futures = this.storeExecutor.invokeAll( urls.stream().map(url -> @@ -229,7 +241,7 @@ public class LiveOperationsCollector implements OperationsCollector { this.collectDiscoveredServer( url, authContext, includeMetrics, now)) .collect(Collectors.toList()), - this.storeDeadlineMillis, TimeUnit.MILLISECONDS); + remainingMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); sources.put("server", unavailable("upstream_interrupted", now)); @@ -292,10 +304,25 @@ public class LiveOperationsCollector implements OperationsCollector { if (!this.pdEnabled || this.serverClients == null) { return Collections.emptyList(); } - return this.serverClients.urls().stream() - .filter(url -> url != null && !url.trim().isEmpty()) - .distinct() - .collect(Collectors.toList()); + Future<List<String>> discovery = this.storeExecutor.submit(() -> + this.serverClients.urls().stream() + .filter(url -> url != null && !url.trim().isEmpty()) + .distinct() + .collect(Collectors.toList())); + try { + return discovery.get(this.storeDeadlineMillis, + TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + discovery.cancel(true); + throw new UpstreamRequestException("upstream_deadline", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + discovery.cancel(true); + throw new UpstreamRequestException("upstream_interrupted", e); + } catch (ExecutionException e) { + throw new UpstreamRequestException("upstream_unavailable", + e.getCause()); + } } private static ServerClientProvider serverClients( @@ -659,7 +686,12 @@ public class LiveOperationsCollector implements OperationsCollector { result = StoreMetricResult.failure( job, "upstream_unavailable", now); } - nodes.set(job.getNodeIndex(), result.getNode()); + Node node = result.getNode(); + if (result.getSuccessfulGroups() == 0 && + directStoreFailure(result.getFailureReason())) { + node = copyNodeWithStatus(node, "DOWN"); + } + nodes.set(job.getNodeIndex(), node); successfulGroups += result.getSuccessfulGroups(); if (result.getFailureReason() != null) { partial = true; @@ -714,11 +746,8 @@ public class LiveOperationsCollector implements OperationsCollector { statuses.put(group, metricStatus(e, now, true)); } } - Node node = copyNode(job.getNode(), metrics, statuses); - if (successfulGroups == 0 && directStoreFailure(failureReason)) { - node = copyNodeWithStatus(node, "DOWN"); - } - return new StoreMetricResult(node, successfulGroups, failureReason); + return new StoreMetricResult(copyNode(job.getNode(), metrics, statuses), + successfulGroups, failureReason); } private static boolean directStoreFailure(String reason) { @@ -988,8 +1017,10 @@ public class LiveOperationsCollector implements OperationsCollector { return "malformed_response"; } String message = error.getMessage(); - if ("upstream_timeout".equals(message)) { - return "upstream_timeout"; + if ("upstream_timeout".equals(message) || + "upstream_deadline".equals(message) || + "upstream_interrupted".equals(message)) { + return message; } if (message != null && message.startsWith("upstream_http_status_")) { return "upstream_rejected"; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java index 5d809fb41..663b805ad 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java @@ -219,15 +219,15 @@ public class OperationsPayloadParser { if (value == null) { continue; } - if (line.startsWith("process_uptime_seconds{")) { + if (prometheusMetric(line, "process_uptime_seconds")) { result.put("uptime_seconds", value); - } else if (line.startsWith("system_cpu_count{")) { + } else if (prometheusMetric(line, "system_cpu_count")) { result.put("cpu_count", value); - } else if (line.startsWith("jvm_threads_live_threads{")) { + } else if (prometheusMetric(line, "jvm_threads_live_threads")) { result.put("threads_live", value); - } else if (line.startsWith("process_cpu_usage{")) { + } else if (prometheusMetric(line, "process_cpu_usage")) { result.put("process_cpu_usage", value); - } else if (line.startsWith("system_cpu_usage{")) { + } else if (prometheusMetric(line, "system_cpu_usage")) { result.put("system_cpu_usage", value); } else if (line.startsWith("jvm_memory_used_bytes{") && line.contains("area=\"heap\"")) { @@ -251,6 +251,14 @@ public class OperationsPayloadParser { return result; } + private static boolean prometheusMetric(String line, String name) { + if (!line.startsWith(name) || line.length() == name.length()) { + return false; + } + char boundary = line.charAt(name.length()); + return boundary == '{' || Character.isWhitespace(boundary); + } + private JsonNode data(String payload, String source) { try { JsonNode root = this.mapper.readTree(payload); diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java index 412edbfcc..3cb79e77e 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java @@ -323,6 +323,23 @@ public class AccountMutationAuthorizationTest { captor.getValue().getId()); } + @Test + public void testEditingOwnPasswordClearsCachedSessionCredentials() { + TestUserController controller = accountController("admin", "ADMIN"); + UserEntity current = account("canonical-id", "admin", true); + Mockito.when(this.authorizationService.get(this.client, + "canonical-id")) + .thenReturn(current); + UserEntity update = new UserEntity(); + update.setPassword("new-password"); + + controller.update("canonical-id", update); + + Mockito.verify(this.authorizationService) + .update(this.client, update); + Assert.assertTrue(controller.authSessionCleared()); + } + @Test public void testDeleteUsesFetchedCanonicalUserId() { TestUserController controller = accountController("admin", "ADMIN"); diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java index 1b7825418..f20cbf613 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java @@ -273,6 +273,29 @@ public class DefaultOperationsDataServiceTest { facts.get("capacity_total_bytes")); } + @SuppressWarnings("unchecked") + @Test + public void testPdFailureKeepsFreshStoreCounts() { + AtomicInteger calls = new AtomicInteger(); + OperationsCollector collector = (client, metrics) -> + calls.getAndIncrement() == 0 ? + factSnapshot("AVAILABLE", "AVAILABLE", 100L, 1000L, 3L, 3L) : + factSnapshot("UNAVAILABLE", "AVAILABLE", null, 2000L, 2L, 2L); + DefaultOperationsDataService service = new DefaultOperationsDataService( + collector, 5, CLOCK); + Set<String> capabilities = Set.of( + OperationsCapabilityService.HEALTH_READ, + OperationsCapabilityService.TOPOLOGY_READ); + + service.overview(client("token-a"), capabilities, false); + Map<String, Object> result = service.overview(client("token-a"), + capabilities, true); + + Map<String, Long> facts = (Map<String, Long>) result.get("facts"); + Assert.assertEquals(Long.valueOf(2L), facts.get("stores")); + Assert.assertEquals(Long.valueOf(2L), facts.get("stores_up")); + } + @SuppressWarnings("unchecked") @Test public void testStoreFailureKeepsFreshPdFacts() { @@ -552,6 +575,14 @@ public class DefaultOperationsDataServiceTest { private static Snapshot factSnapshot(String pdAvailability, String storesAvailability, Long dataSize, Long capacity) { + return factSnapshot(pdAvailability, storesAvailability, dataSize, + capacity, 3L, 3L); + } + + private static Snapshot factSnapshot(String pdAvailability, + String storesAvailability, + Long dataSize, Long capacity, + Long stores, Long storesUp) { Map<String, SourceStatus> sources = new LinkedHashMap<>(); sources.put("pd", factSource(pdAvailability)); sources.put("stores", factSource(storesAvailability)); @@ -563,6 +594,12 @@ public class DefaultOperationsDataServiceTest { facts.put("capacity_total_bytes", capacity); facts.put("capacity_used_bytes", capacity / 2L); } + if (stores != null) { + facts.put("stores", stores); + } + if (storesUp != null) { + facts.put("stores_up", storesUp); + } String status = "AVAILABLE".equals(pdAvailability) && "AVAILABLE".equals(storesAvailability) ? "UP" : "DEGRADED"; diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java index 9869b963e..b075a8507 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java @@ -122,6 +122,49 @@ public class LiveOperationsCollectorTest { .getAvailability()); } + @Test + public void testPdServerDiscoveryUsesOperationsDeadline() + throws IOException { + HttpServer pd = pdServer(200, cluster(), 200, stores()); + LiveOperationsCollector.ServerClientProvider servers = + new LiveOperationsCollector.ServerClientProvider() { + @Override + public java.util.List<String> urls() { + try { + Thread.sleep(10000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + return Collections.emptyList(); + } + + @Override + public HugeClient create(String url, String authContext, + int timeout) { + throw new AssertionError("Server probe must not start"); + } + }; + LiveOperationsCollector collector = collector(true, pd, servers, 50); + + long started = System.nanoTime(); + Snapshot snapshot; + try { + snapshot = collector.collect(serverClient(), false); + } finally { + collector.close(); + pd.stop(0); + } + long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started); + + Assert.assertTrue(elapsed < 3000L); + Assert.assertEquals("UNAVAILABLE", + snapshot.getSources().get("server") + .getAvailability()); + Assert.assertEquals("upstream_deadline", + snapshot.getSources().get("server").getReason()); + } + @Test public void testNonPdModeIsDownWhenOnlySupportedSourceFails() { Snapshot snapshot = collector(false, null).collect( @@ -821,6 +864,10 @@ public class LiveOperationsCollectorTest { .getAvailability()); Assert.assertEquals("upstream_deadline", snapshot.getSources().get("stores").getReason()); + Assert.assertEquals("DOWN", + snapshot.getSources().get("stores").getStatus()); + Assert.assertEquals(Long.valueOf(0L), + snapshot.getFacts().get("stores_up")); Assert.assertEquals(0, http.activeRequests()); } @@ -850,6 +897,14 @@ public class LiveOperationsCollectorTest { .filter(node -> "upstream_deadline".equals( node.getMetricStatuses().get("system").getReason())) .count()); + Assert.assertEquals(1L, snapshot.getNodes().stream() + .filter(node -> "STORE".equals(node.getType())) + .filter(node -> "DOWN".equals(node.getStatus())) + .count()); + Assert.assertEquals("DEGRADED", + snapshot.getSources().get("stores").getStatus()); + Assert.assertEquals(Long.valueOf(1L), + snapshot.getFacts().get("stores_up")); } @Test @@ -894,13 +949,20 @@ public class LiveOperationsCollectorTest { private static LiveOperationsCollector collector( boolean pdEnabled, HttpServer pd, LiveOperationsCollector.ServerClientProvider servers) { + return collector(pdEnabled, pd, servers, 5000); + } + + private static LiveOperationsCollector collector( + boolean pdEnabled, HttpServer pd, + LiveOperationsCollector.ServerClientProvider servers, + int deadlineMillis) { String pdBase = "http://127.0.0.1:" + pd.getAddress().getPort(); return new LiveOperationsCollector( pdEnabled, pdBase, "hubble", "secret", "store-hubble", "store-secret", "server-under-test", new OperationsHttpClient(1000, 1000, 8192), new OperationsPayloadParser(new ObjectMapper()), CLOCK, - 16, 5000, Collections.singleton(pdBase), servers); + 16, deadlineMillis, Collections.singleton(pdBase), servers); } private static LiveOperationsCollector collector(RecordingHttpClient http, diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java index 6de3107b1..e5bb3ea72 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java @@ -262,6 +262,25 @@ public class OperationsPayloadParserTest { Assert.assertFalse(metrics.toString().contains("secret")); } + @Test + public void testParsesUnlabelledPdPrometheusMetrics() { + OperationsPayloadParser parser = new OperationsPayloadParser(MAPPER); + String payload = "process_uptime_seconds 12\n" + + "system_cpu_count 2\n" + + "jvm_threads_live_threads 8\n" + + "process_cpu_usage 0.1\n" + + "system_cpu_usage 0.2\n" + + "process_uptime_seconds_total 99\n"; + + Map<String, Object> metrics = parser.parsePdPrometheusMetrics(payload); + + Assert.assertEquals(12D, metrics.get("uptime_seconds")); + Assert.assertEquals(2D, metrics.get("cpu_count")); + Assert.assertEquals(8D, metrics.get("threads_live")); + Assert.assertEquals(0.1D, metrics.get("process_cpu_usage")); + Assert.assertEquals(0.2D, metrics.get("system_cpu_usage")); + } + @Test(expected = MalformedUpstreamException.class) public void testRejectsPrometheusWithoutRecognizedMetrics() { OperationsPayloadParser parser = new OperationsPayloadParser(MAPPER); 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 index 07e528fb1..3e0e235ad 100644 --- 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 @@ -85,8 +85,8 @@ public class ConfigControllerTest { Assert.assertEquals(Map.of("pd_enabled", true, "server_capabilities_verified", false, "auth_enabled", true, - "graph_create_enabled", true, - "cypher_enabled", true), result); + "graph_create_enabled", false, + "cypher_enabled", false), result); } @Test 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 ca0f4f05e..b558e94e7 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 @@ -271,8 +271,8 @@ public class GraphsControllerCanonicalTest { } @Override - protected void requireGraphSpaceAccess(HugeClient client, - String graphSpace) { + protected void requireGraphSpaceWrite(HugeClient client, + String graphSpace) { this.checkedGraphSpace = graphSpace; } } diff --git a/hugegraph-hubble/hubble-fe/src/App.js b/hugegraph-hubble/hubble-fe/src/App.js index 70dee756e..2043e1955 100644 --- a/hugegraph-hubble/hubble-fe/src/App.js +++ b/hugegraph-hubble/hubble-fe/src/App.js @@ -25,6 +25,7 @@ import Layout from './layout.ant'; import {AuthContextProvider} from './auth/AuthContext'; import * as api from './api'; import {setConfig} from './utils/config'; +import {clearLogin} from './utils/user'; import {useEffect, useState} from 'react'; const CONFIG_RETRY_DELAY_MS = 2000; @@ -45,6 +46,7 @@ function App() { let active = true; let hasSafeConfig = false; let mountedConfigSignature; + let mountedAuthEnabled; let retryTimer; const loadConfig = () => { api.config.getConfig().then(response => { @@ -54,6 +56,17 @@ function App() { if (active) { hasSafeConfig = true; const nextSignature = routeConfigSignature(response.data); + const nextAuthEnabled + = response.data.auth_enabled !== false; + const verified + = response.data.server_capabilities_verified !== false; + if (verified && mountedAuthEnabled !== false + && !nextAuthEnabled) { + clearLogin(); + } + if (verified) { + mountedAuthEnabled = nextAuthEnabled; + } setConfig(response.data); if (nextSignature !== mountedConfigSignature) { mountedConfigSignature = nextSignature; diff --git a/hugegraph-hubble/hubble-fe/src/App.test.js b/hugegraph-hubble/hubble-fe/src/App.test.js index fa2d95a2a..c8584ae6a 100644 --- a/hugegraph-hubble/hubble-fe/src/App.test.js +++ b/hugegraph-hubble/hubble-fe/src/App.test.js @@ -87,6 +87,8 @@ test('shows a retry surface when configuration bootstrap fails', async () => { test('revalidates fail-closed server capabilities until verified', async () => { jest.useFakeTimers(); + sessionStorage.setItem('user_', JSON.stringify({user_name: 'admin'})); + localStorage.setItem('user', 'admin'); api.config.getConfig .mockResolvedValueOnce({ status: 200, @@ -127,6 +129,8 @@ test('revalidates fail-closed server capabilities until verified', async () => { }); expect(screen.getByTestId('app-route')) .toHaveAttribute('data-auth-enabled', 'false'); + expect(sessionStorage.getItem('user_')).toBeNull(); + expect(localStorage.getItem('user')).toBeNull(); jest.useRealTimers(); }); diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js index e83b6afd3..39c33392d 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js @@ -97,7 +97,7 @@ const EditLayer = ({ throw res; }); }, [onCancel, onCreated, refresh, t]); - const updateUser = useCallback(values => { + const updateUser = useCallback(async values => { const profile = toProfilePayload(values); const superAdminChanged = permissionPresetsSupported && Boolean(values.is_superadmin) !== Boolean(detail.is_superadmin); @@ -109,17 +109,29 @@ const EditLayer = ({ : PERMISSION_PRESETS.GS_READ_ONLY, }) : profile; - return api.auth.updateUser(data.id, payload, PAGE_ERROR_CONFIG).then(res => { - if (res.status === 200) { - message.success(t('common.msg.update_success')); - onCancel(); - refresh(); - - return; + const requestUpdate = async update => { + const response = await api.auth.updateUser( + data.id, update, PAGE_ERROR_CONFIG + ); + if (response.status !== 200) { + throw response; } - - throw res; - }); + }; + if (superAdminChanged && profile.user_password) { + await requestUpdate(toPermissionPayload({ + user_name: profile.user_name, + permission_preset: values.is_superadmin + ? PERMISSION_PRESETS.SUPER_ADMIN + : PERMISSION_PRESETS.GS_READ_ONLY, + })); + await requestUpdate(profile); + } + else { + await requestUpdate(payload); + } + message.success(t('common.msg.update_success')); + onCancel(); + refresh(); }, [ data.id, detail.is_superadmin, 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 8f8172ee4..2b88966dd 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 @@ -317,6 +317,48 @@ test('updates a password without resubmitting unchanged permissions', async () = expect(payload).not.toHaveProperty('graphspace_permissions'); }); +test('separates simultaneous password and super administrator changes', async () => { + mockAuthContext = { + capabilities: ['accounts_manage', 'account_permission_presets'], + }; + api.auth.getUserInfo.mockResolvedValue({ + status: 200, + data: { + user_name: 'alice', + is_superadmin: false, + permission_preset: 'GS_READ_ONLY', + graphspace_permissions: [], + }, + }); + api.auth.updateUser.mockResolvedValue({status: 200}); + + render(<EditLayer {...props} data={{id: 'alice'}} op='edit' />); + + await screen.findByDisplayValue('alice'); + fireEvent.change(screen.getByPlaceholderText( + 'account.form.default_password_placeholder' + ), {target: {value: 'new-password'}}); + fireEvent.click(screen.getByRole('switch')); + fireEvent.click(document.querySelector( + '.ant-modal-footer .ant-btn-primary' + )); + + await waitFor(() => expect(api.auth.updateUser).toHaveBeenCalledTimes(2)); + expect(api.auth.updateUser.mock.calls[0][1]).toEqual(expect.objectContaining({ + user_name: 'alice', + permission_preset: 'SUPER_ADMIN', + is_superadmin: true, + })); + expect(api.auth.updateUser.mock.calls[0][1]) + .not.toHaveProperty('user_password'); + expect(api.auth.updateUser.mock.calls[1][1]).toEqual(expect.objectContaining({ + user_name: 'alice', + user_password: 'new-password', + })); + expect(api.auth.updateUser.mock.calls[1][1]) + .not.toHaveProperty('permission_preset'); +}); + test('keeps GraphSpace membership out of the account profile form', async () => { mockAuthContext = { capabilities: ['accounts_manage', 'account_permission_presets'], diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js index f672c4120..dc32f96d2 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js @@ -66,7 +66,7 @@ jest.mock('./SpaceAccess', () => props => ( </button> )} {props.pendingAccount && ( - <span> + <span data-testid="pending-account"> pending {props.pendingAccount.user_name} {' '} {props.pendingAccount.graphspaces.join(',')} @@ -242,6 +242,32 @@ test('super administrators retain all account management actions', async () => { expect(screen.getByText('common.action.delete')).toBeInTheDocument(); }); +test('preserves every GraphSpace when managing an account membership', async () => { + api.auth.getAllUserList.mockResolvedValue({ + status: 200, + data: { + records: [{ + id: 'analyst-id', + user_name: 'analyst', + graphspace_permissions: [ + {graphspace: 'SPACE_A', permission_preset: 'GS_READ_ONLY'}, + {graphspace: 'SPACE_B', permission_preset: 'GS_READ_ONLY'}, + ], + }], + total: 1, + }, + }); + + render(<Account />); + + expect(await screen.findByText('analyst')).toBeInTheDocument(); + await userEvent.click(screen.getByRole('button', {name: 'account.action.more'})); + await userEvent.click(screen.getByText('account.action.manage_membership')); + + expect(await screen.findByTestId('pending-account')) + .toHaveTextContent('pending analyst SPACE_A,SPACE_B'); +}); + test('returns a guided account creation to the selected GraphSpaces', async () => { mockAuthContext.context.actions.members = ['read', 'add', 'remove']; api.auth.getAllUserList.mockResolvedValue({ diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js index 6c5ab50b3..59b037882 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js @@ -95,10 +95,12 @@ const GlobalAccounts = ({ }, []); const showMembership = useCallback(row => { + const graphspaces = getPresetSpaces(row); onAssignMember?.({ user_id: row.id, user_name: row.user_name, - graphspace: getPresetSpaces(row)[0], + graphspace: graphspaces[0], + graphspaces, }); }, [onAssignMember]);
