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 ff1334c765554744cedeff443c34a9a52acf1a44 Author: dark <[email protected]> AuthorDate: Sat Aug 29 13:26:06 2026 +0800 fix(hubble): complete legacy auth compatibility - support the scoped Basic Gremlin fallback - require relogin after password changes - detect anonymous mode and gate unsupported features - fetch one exact Server revision in CI --- .github/workflows/hubble-ci.yml | 25 +++++-- .../org/apache/hugegraph/driver/HugeClient.java | 25 +++++++ .../hugegraph/driver/ServerCompatibility.java | 31 +++++++-- .../hugegraph/driver/ServerCompatibilityTest.java | 9 ++- hugegraph-hubble/AGENTS.md | 12 ++-- hugegraph-hubble/README.md | 17 ++--- .../java/org/apache/hugegraph/common/Constant.java | 7 ++ .../org/apache/hugegraph/config/HubbleConfig.java | 24 +------ .../hugegraph/controller/BaseController.java | 50 +++++++++++++- .../hugegraph/controller/ConfigController.java | 48 ++++++++++++- .../controller/algorithm/OltpAlgoController.java | 52 +++++++-------- .../hugegraph/controller/auth/LoginController.java | 7 ++ .../hugegraph/controller/auth/UserController.java | 8 ++- .../controller/langchain/LangChainController.java | 2 +- .../controller/saas/SaasGraphViewController.java | 4 +- .../controller/schema/SchemaController.java | 2 +- .../apache/hugegraph/options/HubbleOptions.java | 8 --- .../hugegraph/service/HugeClientPoolService.java | 20 +++++- .../hugegraph/service/auth/AuthContextService.java | 11 +-- .../hugegraph/service/auth/AuthModeService.java | 46 +++++++++++-- .../config/HubbleConfigEnvironmentTest.java | 68 ------------------- .../auth/AccountMutationAuthorizationTest.java | 19 ++++++ .../controller/ingest/IngestControllerTest.java | 6 +- .../controller/space/GraphSpaceControllerTest.java | 9 +-- .../SchemaTemplateControllerSecurityTest.java | 7 +- .../service/auth/AuthContextServiceTest.java | 8 ++- .../service/auth/AuthModeServiceTest.java | 55 +++++++++++++++ .../apache/hugegraph/unit/AuthSecurityTest.java | 57 +++++++++++----- .../unit/BaseControllerGremlinClientTest.java | 27 ++++++-- .../hugegraph/unit/ConfigControllerTest.java | 78 +++++++++++++++++++++- .../hugegraph/unit/OperationsControllerTest.java | 5 +- .../org/apache/hugegraph/unit/UnitTestSuite.java | 7 +- .../static/conf/hugegraph-hubble.properties | 3 - .../assembly/travis/download-hugegraph.sh | 2 +- .../hubble-fe/src/auth/graphspaceAccess.js | 10 ++- .../hubble-fe/src/auth/graphspaceAccess.test.js | 5 ++ .../src/components/GraphContextSwitcher/index.js | 15 +++-- .../components/GraphContextSwitcher/index.test.js | 20 ++++++ .../hubble-fe/src/components/Sidebar/index.ant.js | 7 +- .../src/components/Sidebar/index.ant.test.js | 25 +++++++ .../src/i18n/resources/en-US/modules/pages.json | 2 + .../src/i18n/resources/zh-CN/modules/pages.json | 2 + .../hubble-fe/src/modules/analysis/Home/index.js | 3 + .../src/modules/analysis/QueryBar/Home/index.js | 5 +- .../modules/analysis/QueryBar/Home/index.test.js | 15 +++++ .../src/pages/Graph/default-card-actions.test.js | 5 +- .../hubble-fe/src/pages/Graph/index.js | 45 +++++++------ hugegraph-hubble/hubble-fe/src/pages/My/index.js | 12 +++- .../hubble-fe/src/pages/My/my-recovery.test.js | 34 ++++++++-- hugegraph-hubble/hubble-fe/src/utils/config.js | 17 ++++- .../src/utils/{config.js => config.test.js} | 36 +++++----- .../hubble-fe/src/utils/productMode.js | 3 - .../hubble-fe/src/utils/productMode.test.js | 5 +- 53 files changed, 736 insertions(+), 289 deletions(-) diff --git a/.github/workflows/hubble-ci.yml b/.github/workflows/hubble-ci.yml index ae7290aa4..425e09d76 100644 --- a/.github/workflows/hubble-ci.yml +++ b/.github/workflows/hubble-ci.yml @@ -24,8 +24,9 @@ on: env: TRAVIS_DIR: hugegraph-hubble/hubble-dist/assembly/travis # Server PR #3159 declares the GraphSpace default-role contract as API 0.72. - HUGEGRAPH_SERVER_COMMIT: 52035dad9ee8d6b666329ca0d03950c773d3e1eb - HUGEGRAPH_SERVER_FETCH_REF: refs/pull/3159/head + # TODO: After #3159 merges, switch these values to the stable upstream branch. + HUGEGRAPH_SERVER_GIT_URL: https://github.com/hugegraph/hugegraph.git + HUGEGRAPH_SERVER_BRANCH: cx/bump-server-api-version jobs: hubble-ci: @@ -88,11 +89,22 @@ jobs: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('hugegraph-hubble/hubble-fe/yarn.lock') }} + - name: Resolve HugeGraph Server branch + id: server-ref + run: | + SERVER_COMMIT="$(git ls-remote "$HUGEGRAPH_SERVER_GIT_URL" \ + "refs/heads/$HUGEGRAPH_SERVER_BRANCH" | cut -f1)" + if [[ -z "$SERVER_COMMIT" ]]; then + echo "Unable to resolve HugeGraph Server branch" >&2 + exit 1 + fi + echo "commit=$SERVER_COMMIT" >> "$GITHUB_OUTPUT" + - name: Cache HugeGraph Server uses: actions/cache@v6 with: - path: ~/hugegraph-cache-${{ env.HUGEGRAPH_SERVER_COMMIT }} - key: ${{ runner.os }}-hugegraph-server-${{ env.HUGEGRAPH_SERVER_COMMIT }} + path: ~/hugegraph-cache-${{ steps.server-ref.outputs.commit }} + key: ${{ runner.os }}-hugegraph-server-${{ steps.server-ref.outputs.commit }} - name: Frontend i18n check working-directory: hugegraph-hubble/hubble-fe @@ -116,8 +128,9 @@ jobs: - name: Prepare env and service env: - COMMIT_ID: ${{ env.HUGEGRAPH_SERVER_COMMIT }} - COMMIT_REF: ${{ env.HUGEGRAPH_SERVER_FETCH_REF }} + COMMIT_ID: ${{ steps.server-ref.outputs.commit }} + COMMIT_REF: ${{ steps.server-ref.outputs.commit }} + HUGEGRAPH_GIT_URL: ${{ env.HUGEGRAPH_SERVER_GIT_URL }} NODE_OPTIONS: --max-old-space-size=4096 run: | echo "=== Environment Info ===" diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java index c314405f9..b0e4a205d 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java @@ -268,10 +268,35 @@ public class HugeClient implements Closeable { return this.compatibility.supportsDefaultRole(); } + public boolean supportsGraphSpace() { + return this.compatibility.supportsGraphSpace(); + } + + public boolean supportsGraphCreate() { + return ServerCompatibility.supportsGraphCreate( + this.version.getApiVersion()); + } + + public boolean supportsCypher() { + return this.compatibility.supportsCypher(); + } + public boolean supportsPersonalProfileUpdate() { return this.compatibility.supportsPersonalProfileUpdate(); } + public boolean isServerAuthEnabled() { + try { + this.graphs.listGraph(); + return false; + } catch (ServerException e) { + if (e.status() == 401) { + return true; + } + throw e; + } + } + public User findUserByName(String name) { if (this.supportsDefaultRole()) { return this.auth.getUserByName(name); diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java index ac329aacf..67b3d9ead 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/ServerCompatibility.java @@ -29,6 +29,7 @@ import org.apache.hugegraph.util.VersionUtil; public final class ServerCompatibility { private static final String GRAPHSPACE_MIN_VERSION = "1.7.0"; + private static final String GRAPH_CREATE_MIN_API_VERSION = "0.67"; private static final String DEFAULT_ROLE_MIN_API_VERSION = "0.72"; private ServerCompatibility() { @@ -54,12 +55,16 @@ public final class ServerCompatibility { } private static boolean supportsDefaultRoleApi(String apiVersion) { + return supportsApi(apiVersion, DEFAULT_ROLE_MIN_API_VERSION); + } + + private static boolean supportsApi(String apiVersion, + String minimumVersion) { if (apiVersion == null || apiVersion.trim().isEmpty()) { return false; } try { - return VersionUtil.gte(apiVersion.trim(), - DEFAULT_ROLE_MIN_API_VERSION); + return VersionUtil.gte(apiVersion.trim(), minimumVersion); } catch (RuntimeException ignored) { return false; } @@ -69,6 +74,14 @@ public final class ServerCompatibility { return profile(coreVersion).supportsGraphSpace(); } + public static boolean supportsCypher(String coreVersion) { + return profile(coreVersion).supportsCypher(); + } + + public static boolean supportsGraphCreate(String apiVersion) { + return supportsApi(apiVersion, GRAPH_CREATE_MIN_API_VERSION); + } + public static boolean supportsDefaultRole(String coreVersion, String apiVersion) { return profile(coreVersion, apiVersion).supportsDefaultRole(); @@ -81,17 +94,19 @@ public final class ServerCompatibility { } public enum Profile { - LEGACY(false, false, false), - GRAPHSPACE(true, false, false), - MODERN(true, true, true); + LEGACY(false, false, false, false), + GRAPHSPACE(true, true, false, false), + MODERN(true, true, true, true); private final boolean graphSpace; + private final boolean cypher; private final boolean defaultRole; private final boolean personalProfileUpdate; - Profile(boolean graphSpace, boolean defaultRole, + Profile(boolean graphSpace, boolean cypher, boolean defaultRole, boolean personalProfileUpdate) { this.graphSpace = graphSpace; + this.cypher = cypher; this.defaultRole = defaultRole; this.personalProfileUpdate = personalProfileUpdate; } @@ -100,6 +115,10 @@ public final class ServerCompatibility { return this.graphSpace; } + public boolean supportsCypher() { + return this.cypher; + } + public boolean supportsDefaultRole() { return this.defaultRole; } diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java index 577071974..783508326 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/ServerCompatibilityTest.java @@ -25,14 +25,21 @@ public class ServerCompatibilityTest { @Test public void shouldKeepLegacyServersConservative() { Assert.assertFalse(ServerCompatibility.supportsGraphSpace("1.5.0")); - Assert.assertFalse(ServerCompatibility.supportsGraphSpace("1.6.0")); + Assert.assertFalse(ServerCompatibility.supportsCypher("1.5.0")); Assert.assertFalse(ServerCompatibility.supportsGraphSpace(null)); + Assert.assertFalse(ServerCompatibility.supportsCypher(null)); + Assert.assertFalse(ServerCompatibility.supportsGraphCreate(null)); Assert.assertFalse(ServerCompatibility.supportsGraphSpace("not-a-version")); + Assert.assertFalse(ServerCompatibility.supportsCypher("not-a-version")); + Assert.assertFalse(ServerCompatibility.supportsGraphCreate("not-a-version")); + Assert.assertFalse(ServerCompatibility.supportsGraphCreate("0.66")); } @Test public void shouldExposeGraphSpaceForModernServers() { Assert.assertTrue(ServerCompatibility.supportsGraphSpace("1.7.0")); + Assert.assertTrue(ServerCompatibility.supportsCypher("1.7.0")); + Assert.assertTrue(ServerCompatibility.supportsGraphCreate("0.67")); Assert.assertTrue(ServerCompatibility.supportsGraphSpace(" 1.7.0 ")); Assert.assertTrue(ServerCompatibility.supportsGraphSpace("1.8.0")); Assert.assertFalse(ServerCompatibility.supportsDefaultRole( diff --git a/hugegraph-hubble/AGENTS.md b/hugegraph-hubble/AGENTS.md index 9f732a344..4793edafc 100644 --- a/hugegraph-hubble/AGENTS.md +++ b/hugegraph-hubble/AGENTS.md @@ -2,12 +2,12 @@ ## Authentication and connection boundary -The `1.8/master` path is the source of truth. Backend configuration exposes -one `auth.enabled` switch and 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. +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 diff --git a/hugegraph-hubble/README.md b/hugegraph-hubble/README.md index 445b4ee20..fde297990 100644 --- a/hugegraph-hubble/README.md +++ b/hugegraph-hubble/README.md @@ -10,17 +10,12 @@ graph data load, schema management, graph relationship analysis, and graphical d ## Authentication, connections, and compatibility Hubble uses one capability-driven connection boundary for `1.8/master`. -`auth.enabled=true` creates an authenticated session; when it is `false`, Hubble -uses an unauthenticated client and does not create a fake user. Account and -permission entry points are hidden in anonymous mode. Connection switching -always goes through the backend resolver. In PD mode, a valid server address -returned by discovery is sufficient; a manually configured server URL is not -required. - -Container and orchestrated deployments can set `HUBBLE_AUTH_ENABLED=true` or -`false`. This explicit runtime value overrides `auth.enabled` from the -properties file, and invalid values fail startup instead of silently selecting -an authentication mode. +Authentication mode is detected from the connected HugeGraph Server; Hubble +does not maintain a separate authentication switch. Account and permission +entry points are hidden when Server allows anonymous access. Connection +switching always goes through the backend resolver. In PD mode, a valid server +address returned by discovery is sufficient; a manually configured server URL +is not required. The UI presents four stable permission meanings: super administrator, GraphSpace read-only, GraphSpace read-write, and GraphSpace administrator. The last one 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 63be9e458..66f94ed6a 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,13 @@ public final class Constant { public static final String TOKEN_KEY = "auth_token"; public static final String USERNAME_KEY = "username"; + /** + * Server-side-only legacy Gremlin credentials. 1.7's Gremlin HTTP + * channel accepts Basic auth while its REST APIs accept the login token. + */ + public static final String PASSWORD_KEY = "auth_password"; + public static final String PASSWORD_EXPIRE_AT_KEY = + "auth_password_expire_at"; public static final String GRAPHSPACE_ACCESS_KEY = "validated_graphspace"; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java index ea40f8668..8caad2787 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/config/HubbleConfig.java @@ -28,14 +28,10 @@ import org.springframework.context.annotation.Configuration; import java.io.File; import java.net.URL; -import java.util.Locale; -import java.util.Map; @Configuration public class HubbleConfig { - static final String AUTH_ENABLED_ENV = "HUBBLE_AUTH_ENABLED"; - @Autowired private ApplicationArguments arguments; @@ -60,24 +56,6 @@ public class HubbleConfig { conf = path; } } - HugeConfig config = new HugeConfig(conf); - applyEnvironmentOverrides(config, System.getenv()); - return config; - } - - static void applyEnvironmentOverrides(HugeConfig config, - Map<String, String> environment) { - String authEnabled = environment.get(AUTH_ENABLED_ENV); - if (authEnabled == null) { - return; - } - - String normalized = authEnabled.trim().toLowerCase(Locale.ROOT); - if (!normalized.equals("true") && !normalized.equals("false")) { - throw new ExternalException( - AUTH_ENABLED_ENV + " must be true or false"); - } - config.setProperty(HubbleOptions.AUTH_ENABLED.name(), - Boolean.valueOf(normalized)); + return new HugeConfig(conf); } } 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 1f7634785..eb7cffa4f 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 @@ -21,6 +21,7 @@ package org.apache.hugegraph.controller; import java.util.List; import java.util.function.Function; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.driver.factory.PDHugeClientFactory; @@ -143,6 +144,8 @@ public abstract class BaseController { protected void clearAuthSession() { this.delSession(Constant.TOKEN_KEY); this.delSession(Constant.USERNAME_KEY); + this.delSession(Constant.PASSWORD_KEY); + this.delSession(Constant.PASSWORD_EXPIRE_AT_KEY); } protected HugeClient authClient(String graphSpace, String graph) { @@ -217,7 +220,52 @@ public abstract class BaseController { } protected HugeClient authGremlinClient(String graphSpace, String graph) { - return this.authClient(graphSpace, graph); + if (this.authMode != null && this.authMode.anonymous()) { + return this.authClient(graphSpace, graph); + } + + HttpServletRequest request = this.getRequest(); + HttpSession session = request.getSession(false); + if (session == null) { + return this.authClient(graphSpace, graph); + } + + String username = (String) session.getAttribute(Constant.USERNAME_KEY); + String token = (String) session.getAttribute(Constant.TOKEN_KEY); + String password = this.validSessionPassword(session); + if (!StringUtils.hasText(username) || !StringUtils.hasText(token) || + !StringUtils.hasText(password)) { + return this.authClient(graphSpace, graph); + } + + Object existing = request.getAttribute("hugeClient"); + if (existing instanceof HugeClient) { + ((HugeClient) existing).close(); + } + HugeClient client = this.createBasicClient(graphSpace, graph, + username, password); + this.requireGraphSpaceAccess(client, graphSpace); + request.setAttribute("hugeClient", client); + return client; + } + + protected HugeClient createBasicClient(String graphSpace, String graph, + String username, String password) { + return this.hugeClientPoolService.createBasicClient( + graphSpace, graph, username, password); + } + + private String validSessionPassword(HttpSession session) { + Object password = session.getAttribute(Constant.PASSWORD_KEY); + Object expiresAt = session.getAttribute( + Constant.PASSWORD_EXPIRE_AT_KEY); + if (!(password instanceof String) || !(expiresAt instanceof Number) || + System.currentTimeMillis() >= ((Number) expiresAt).longValue()) { + session.removeAttribute(Constant.PASSWORD_KEY); + session.removeAttribute(Constant.PASSWORD_EXPIRE_AT_KEY); + return null; + } + return (String) password; } protected HugeClient unauthClient() { 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 8f8f5cb6c..8ee22aa1c 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 @@ -23,7 +23,10 @@ import java.util.Map; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.HugeClientPoolService; +import org.apache.hugegraph.service.auth.AuthModeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -33,14 +36,57 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping(Constant.API_VERSION + "config") public class ConfigController { + private static final int CAPABILITY_PROBE_TIMEOUT_SECONDS = 3; + @Autowired private HugeConfig config; + @Autowired(required = false) + private HugeClientPoolService hugeClientPoolService; + + @Autowired + private AuthModeService authModeService; + @GetMapping public Map<String, Object> getConfig() { Map<String, Object> result = new HashMap<>(); result.put("pd_enabled", config.get(HubbleOptions.PD_ENABLED)); - result.put("auth_enabled", config.get(HubbleOptions.AUTH_ENABLED)); + result.putAll(this.serverCapabilities()); return result; } + + private Map<String, Object> serverCapabilities() { + Map<String, Object> capabilities = new HashMap<>(); + boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED); + if (pdEnabled) { + capabilities.put("auth_enabled", this.authModeService.enabled()); + capabilities.put("graph_create_enabled", true); + capabilities.put("cypher_enabled", true); + return capabilities; + } + capabilities.put("auth_enabled", true); + capabilities.put("graph_create_enabled", false); + capabilities.put("cypher_enabled", false); + if (hugeClientPoolService == null) { + return capabilities; + } + + try (HugeClient client = this.createUnauthClient()) { + capabilities.put("auth_enabled", + this.authModeService.update( + client.isServerAuthEnabled())); + capabilities.put("graph_create_enabled", + client.supportsGraphCreate()); + capabilities.put("cypher_enabled", client.supportsCypher()); + return capabilities; + } catch (RuntimeException ignored) { + // Keep bootstrap resilient when the Server is temporarily unavailable. + return capabilities; + } + } + + protected HugeClient createUnauthClient() { + return this.hugeClientPoolService.createUnauthClient( + CAPABILITY_PROBE_TIMEOUT_SECONDS); + } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java index c50d77ab0..05408442d 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OltpAlgoController.java @@ -95,7 +95,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult rings(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody RingsEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.rings(client, body); } @@ -103,7 +103,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult advancedPaths(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody PathsRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.advancedpaths(client, body); } @@ -111,7 +111,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult sameNeighbors(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody SameNeighborsEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.sameNeighbors(client, body); } @@ -119,7 +119,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult kout(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody KoutEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.kout(client, body); } @@ -127,7 +127,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult koutPost(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody KoutRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.koutPost(client, body); } @@ -135,7 +135,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult kneighbor(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody KneighborEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.kneighbor(client, body); } @@ -143,7 +143,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult kneighborPost(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody KneighborRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.kneighborPost(client, body); } @@ -151,7 +151,7 @@ public class OltpAlgoController extends BaseController { public JaccardsimilarityView jaccardSimilarity(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody JaccardSimilarityEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.jaccardSimilarity(client, body); } @@ -160,7 +160,7 @@ public class OltpAlgoController extends BaseController { @PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody SingleSourceJaccardSimilarityRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.jaccardSimilarityPost(client, body); } @@ -168,7 +168,7 @@ public class OltpAlgoController extends BaseController { public RanksView personalRank(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody PersonalRankAPI.Request body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.personalRank(client, body); } @@ -176,7 +176,7 @@ public class OltpAlgoController extends BaseController { public RanksView neighborRank(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody NeighborRankAPI.Request body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.neighborRank(client, body); } @@ -184,7 +184,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult allShortPaths(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody AllShortestPathsEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.allShortestPaths(client, body); } @@ -199,7 +199,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult weightedShortestPath(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody WeightedShortestPathEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.weightedShortestPath(client, body); } @@ -208,7 +208,7 @@ public class OltpAlgoController extends BaseController { @PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody SingleSourceShortestPathEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.singleSourceShortestPath(client, body); } @@ -216,7 +216,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult multiNodeShortestPath(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody MultiNodeShortestPathRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.multiNodeShortestPath(client, body); } @@ -224,7 +224,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult paths(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody PathsEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.paths(client, body); } @@ -232,7 +232,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult customizedPaths(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody CustomizedPathsRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.customizedPaths(client, body); } @@ -240,7 +240,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult templatePaths(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody TemplatePathsRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.templatePaths(client, body); } @@ -248,7 +248,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult crosspoints(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody CrossPointsEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.crosspoints(client, body); } @@ -256,7 +256,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult customizedcrosspoints(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody CrosspointsRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.customizedcrosspoints(client, body); } @@ -264,7 +264,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult rays(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody RaysEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.rays(client, body); } @@ -272,7 +272,7 @@ public class OltpAlgoController extends BaseController { public FusiformsimilarityView fusiformsimilarity(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody FusiformSimilarityRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.fusiformsimilarity(client, body); } @@ -280,7 +280,7 @@ public class OltpAlgoController extends BaseController { public Map<String, Double> adamicadar(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody AdamicadarEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.adamicadar(client, body); } @@ -288,7 +288,7 @@ public class OltpAlgoController extends BaseController { public Map<String, Double> resourceallocation(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody ResourceallocationEntity body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.resourceallocation(client, body); } @@ -296,7 +296,7 @@ public class OltpAlgoController extends BaseController { public GremlinResult sameneighborsbatch(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody SameNeighborsBatchRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.sameneighborsbatch(client, body); } @@ -304,7 +304,7 @@ public class OltpAlgoController extends BaseController { public EgonetView egonet(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody EgonetRequest body) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); return this.service.egonet(client, body); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java index c5a51e226..955909cf4 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/LoginController.java @@ -104,6 +104,13 @@ public class LoginController extends BaseController { this.getRequest().changeSessionId(); this.setUser(login.name()); this.setToken(result.token()); + // HugeGraph 1.7's Gremlin HTTP channel only accepts Basic auth. + // Keep the credential server-side for the session lifetime so + // graph queries can use the same identity as REST requests. + this.setSession(Constant.PASSWORD_KEY, login.password()); + this.setSession(Constant.PASSWORD_EXPIRE_AT_KEY, + System.currentTimeMillis() + + TOKEN_EXPIRE_SECONDS * 1000L); return user; } catch (Throwable e) { this.clearAuthSession(); 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 55ff8e9a3..a0c9d2495 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 @@ -131,7 +131,13 @@ public class UserController extends BaseController { "Permission denied: change another account password"); } HugeClient client = this.authClient(null, null); - return userService.updatepwd(client, pwd.getUsername(), pwd.getOldpwd(), pwd.getNewpwd()); + Response response = userService.updatepwd(client, pwd.getUsername(), + pwd.getOldpwd(), + pwd.getNewpwd()); + if (response.getStatus() == Constant.STATUS_OK) { + this.clearAuthSession(); + } + return response; } @GetMapping("listadminspace/{username}") diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/langchain/LangChainController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/langchain/LangChainController.java index 8c1695fe0..3e39a0eb8 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/langchain/LangChainController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/langchain/LangChainController.java @@ -184,7 +184,7 @@ public class LangChainController extends BaseController { requestLangChainParams.userName, requestLangChainParams.password); try { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); JsonView result = this.queryService.executeSingleGremlinQuery(client, query); return result.getData(); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaasGraphViewController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaasGraphViewController.java index fd34bef72..b06c6ac9b 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaasGraphViewController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/saas/SaasGraphViewController.java @@ -84,7 +84,7 @@ public class SaasGraphViewController extends GremlinController { StopWatch timer = StopWatch.createStarted(); try { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); GremlinResult result = this.queryService.executeGremlinQuery(client, query.convert2GremlinQuery()); @@ -124,7 +124,7 @@ public class SaasGraphViewController extends GremlinController { Map<String, Object> result = new HashMap<>(3); try { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); asyncId = this.queryService.executeGremlinAsyncTask(client, query.convert2GremlinQuery()); status = ExecuteStatus.ASYNC_TASK_SUCCESS; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java index 58009368b..d209e8733 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/schema/SchemaController.java @@ -91,7 +91,7 @@ public class SchemaController extends BaseController { public Object addSchemaGroovy(@PathVariable("graphspace") String graphSpace, @PathVariable("graph") String graph, @RequestBody SchemaGroovy schemaGroovy) { - HugeClient client = this.authClient(graphSpace, graph); + HugeClient client = this.authGremlinClient(graphSpace, graph); String content = schemaGroovy.getSchemaGroovy(); log.info("Add schema groovy: {}", content); checkSchemaGroovy(content); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java index 56b544dc3..c04132c32 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/options/HubbleOptions.java @@ -279,14 +279,6 @@ public class HubbleOptions extends OptionHolder { true ); - public static final ConfigOption<Boolean> AUTH_ENABLED = - new ConfigOption<>("auth.enabled", - "Whether Hubble requires a user session. Set false when " + - "the connected HugeGraph Server runs in anonymous mode.", - null, - true - ); - public static final ConfigOption<String> SERVER_URL = new ConfigOption<>( "server.direct_url", diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java index 167f7f5c0..bb92abc6e 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/HugeClientPoolService.java @@ -91,6 +91,10 @@ public final class HugeClientPoolService { return getOrCreate(null, null, null, null); } + public HugeClient createUnauthClient(int timeout) { + return this.create(null, null, null, null, null, null, timeout); + } + public HugeClient createUnauthClient(String graphSpace, String graph) { return getOrCreate(null, graphSpace, graph, null); } @@ -132,6 +136,13 @@ public final class HugeClientPoolService { private HugeClient create(String url, String graphSpace, String graph, String token, String username, String password) { + return this.create(url, graphSpace, graph, token, username, password, + null); + } + + private HugeClient create(String url, String graphSpace, String graph, + String token, String username, String password, + Integer timeoutOverride) { if (StringUtils.isEmpty(url)) { List<String> urls = this.allAvailableURLs(graphSpace, graph); @@ -143,8 +154,9 @@ public final class HugeClientPoolService { if (StringUtils.isEmpty(tmpurl)) { continue; } - HugeClient tmpclient = this.create(tmpurl, graphSpace, graph, token, - username, password); + HugeClient tmpclient = this.create(tmpurl, graphSpace, graph, + token, username, password, + timeoutOverride); if (checkHealth(tmpclient)) { return tmpclient; @@ -171,7 +183,9 @@ public final class HugeClientPoolService { connection.setGraphSpace(graphSpace); connection.setGraph(graph); if (connection.getTimeout() == null) { - int timeout = this.config.get(HubbleOptions.CLIENT_REQUEST_TIMEOUT); + int timeout = timeoutOverride != null ? + timeoutOverride : + this.config.get(HubbleOptions.CLIENT_REQUEST_TIMEOUT); connection.setTimeout(timeout); } this.sslService.configSSL(this.config, connection); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java index 80ad33a69..cc2d45589 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java @@ -84,16 +84,18 @@ public class AuthContextService { private final HugeConfig config; private final UserService users; + private final AuthModeService authMode; @Autowired - public AuthContextService(HugeConfig config, UserService users) { + public AuthContextService(HugeConfig config, UserService users, + AuthModeService authMode) { this.config = config; this.users = users; + this.authMode = authMode; } public Map<String, Object> context(HugeClient client, String username) { - if (Boolean.FALSE.equals( - this.config.get(HubbleOptions.AUTH_ENABLED))) { + if (this.authMode.anonymous()) { return anonymousContext(this.config.get(HubbleOptions.PD_ENABLED)); } boolean pdEnabled = this.config.get(HubbleOptions.PD_ENABLED); @@ -149,8 +151,7 @@ public class AuthContextService { public void requireGraphSpaceWrite(HugeClient client, String username, String graphSpace) { - if (Boolean.FALSE.equals( - this.config.get(HubbleOptions.AUTH_ENABLED)) || + if (this.authMode.anonymous() || !this.config.get(HubbleOptions.PD_ENABLED) || !client.supportsDefaultRole()) { return; diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java index f4f8c8230..ad0382fce 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthModeService.java @@ -18,8 +18,10 @@ package org.apache.hugegraph.service.auth; -import org.apache.hugegraph.config.HugeConfig; -import org.apache.hugegraph.options.HubbleOptions; +import java.util.concurrent.TimeUnit; + +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.service.HugeClientPoolService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -28,20 +30,50 @@ import org.springframework.stereotype.Service; * not infer mode from sessions or PD settings. */ @Service -public final class AuthModeService { +public class AuthModeService { + + private static final int PROBE_TIMEOUT_SECONDS = 3; + private static final long CACHE_NANOS = TimeUnit.SECONDS.toNanos(30L); - private final HugeConfig config; + private final HugeClientPoolService hugeClientPoolService; + private volatile Boolean serverAuthEnabled; + private volatile long detectedAt; @Autowired - public AuthModeService(HugeConfig config) { - this.config = config; + public AuthModeService(HugeClientPoolService hugeClientPoolService) { + this.hugeClientPoolService = hugeClientPoolService; } public boolean enabled() { - return !Boolean.FALSE.equals(this.config.get(HubbleOptions.AUTH_ENABLED)); + Boolean cached = this.serverAuthEnabled; + long now = System.nanoTime(); + if (cached != null && now - this.detectedAt < CACHE_NANOS) { + return cached; + } + try (HugeClient client = this.createUnauthClient()) { + return this.update(client.isServerAuthEnabled(), now); + } catch (RuntimeException ignored) { + // Fail closed while Server state is unavailable. + return this.update(true, now); + } } public boolean anonymous() { return !this.enabled(); } + + public boolean update(boolean enabled) { + return this.update(enabled, System.nanoTime()); + } + + private boolean update(boolean enabled, long detectedAt) { + this.serverAuthEnabled = enabled; + this.detectedAt = detectedAt; + return enabled; + } + + protected HugeClient createUnauthClient() { + return this.hugeClientPoolService.createUnauthClient( + PROBE_TIMEOUT_SECONDS); + } } diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/config/HubbleConfigEnvironmentTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/config/HubbleConfigEnvironmentTest.java deleted file mode 100644 index e15649fce..000000000 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/config/HubbleConfigEnvironmentTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * - * 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.config; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.apache.hugegraph.exception.ExternalException; -import org.apache.hugegraph.options.HubbleOptions; -import org.junit.Assert; -import org.junit.Test; - -public class HubbleConfigEnvironmentTest { - - @Test - public void testAuthModeCanBeOverriddenByEnvironment() { - HugeConfig config = config(true); - - HubbleConfig.applyEnvironmentOverrides( - config, Map.of("HUBBLE_AUTH_ENABLED", "false")); - - Assert.assertFalse(config.get(HubbleOptions.AUTH_ENABLED)); - } - - @Test - public void testMissingEnvironmentKeepsFileValue() { - HugeConfig config = config(false); - - HubbleConfig.applyEnvironmentOverrides(config, Collections.emptyMap()); - - Assert.assertFalse(config.get(HubbleOptions.AUTH_ENABLED)); - } - - @Test(expected = ExternalException.class) - public void testAuthModeRejectsInvalidEnvironmentValue() { - HugeConfig config = config(true); - - HubbleConfig.applyEnvironmentOverrides( - config, Map.of("HUBBLE_AUTH_ENABLED", "disabled")); - } - - private static HugeConfig config(boolean authEnabled) { - if (!OptionSpace.containKey(HubbleOptions.AUTH_ENABLED.name())) { - OptionSpace.register("hubble-environment-test", - HubbleOptions.instance()); - } - Map<String, Object> properties = new HashMap<>(); - properties.put(HubbleOptions.AUTH_ENABLED.name(), authEnabled); - return new HugeConfig(properties); - } -} 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 3eabedb0a..412edbfcc 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 @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -32,6 +33,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.multipart.MultipartFile; import org.apache.hugegraph.controller.BaseController; +import org.apache.hugegraph.common.Constant; +import org.apache.hugegraph.common.Response; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.auth.UserEntity; import org.apache.hugegraph.entity.auth.UserView; @@ -111,9 +114,15 @@ public class AccountMutationAuthorizationTest { .oldpwd("old") .newpwd("new") .build(); + Mockito.when(this.authorizationService.updatepwd( + this.client, "alice", "old", "new")) + .thenReturn(Response.builder() + .status(Constant.STATUS_OK) + .build()); controller.updatepwd(own); Mockito.verify(this.authorizationService) .updatepwd(this.client, "alice", "old", "new"); + Assert.assertTrue(controller.authSessionCleared()); } @Test @@ -605,6 +614,7 @@ public class AccountMutationAuthorizationTest { private final HugeClient client; private final String username; + private boolean authSessionCleared; TestUserController(HugeClient client, String username) { this.client = client; @@ -620,6 +630,15 @@ public class AccountMutationAuthorizationTest { protected String getUser() { return this.username; } + + @Override + protected void clearAuthSession() { + this.authSessionCleared = true; + } + + boolean authSessionCleared() { + return this.authSessionCleared; + } } private static class TestGraphSpaceUserController 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 e1c9841db..d3f765bfd 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 @@ -377,10 +377,10 @@ public class IngestControllerTest { 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); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + Mockito.when(authMode.anonymous()).thenReturn(true); GraphSpaceService graphSpaces = Mockito.mock(GraphSpaceService.class); JobManagerService jobs = Mockito.mock(JobManagerService.class); LoadTaskService loadTasks = Mockito.mock(LoadTaskService.class); @@ -402,7 +402,7 @@ public class IngestControllerTest { 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, "authMode", authMode); this.setField(controller, "graphSpaceAccessService", graphSpaces); this.setField(controller, "jobManagerService", jobs); this.setField(controller, "loadTaskService", loadTasks); 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 8280a659e..e12a24d91 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 @@ -118,8 +118,8 @@ public class GraphSpaceControllerTest { new TestGraphSpaceController(client); HugeConfig config = Mockito.mock(HugeConfig.class); Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true); - Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)).thenReturn(false); - AuthModeService authMode = new AuthModeService(config); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + Mockito.when(authMode.anonymous()).thenReturn(true); Mockito.when(graphSpaceService.listAnonymous(client)) .thenReturn(java.util.Collections.singletonList("public")); Mockito.when(graphSpaceService.getAnonymous(client, "public")) @@ -270,9 +270,10 @@ public class GraphSpaceControllerTest { client, userService, graphSpaceService); HugeConfig config = Mockito.mock(HugeConfig.class); - Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)).thenReturn(false); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + Mockito.when(authMode.anonymous()).thenReturn(true); ReflectionTestUtils.setField(controller, "authMode", - new AuthModeService(config)); + authMode); assertForbidden(() -> controller.add(new GraphSpaceEntity())); diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java index aa31149ad..9a9c50164 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/space/SchemaTemplateControllerSecurityTest.java @@ -142,10 +142,11 @@ public class SchemaTemplateControllerSecurityTest { TestController controller = new TestController(); HugeConfig config = Mockito.mock(HugeConfig.class); Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(true); - Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)) - .thenReturn(authEnabled); controller.config = config; - controller.setAuthMode(new AuthModeService(config)); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + Mockito.when(authMode.enabled()).thenReturn(authEnabled); + Mockito.when(authMode.anonymous()).thenReturn(!authEnabled); + controller.setAuthMode(authMode); controller.schemaTemplateService = Mockito.mock(SchemaTemplateService.class); return controller; diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java index 1091906ff..a697a84bb 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java @@ -256,8 +256,7 @@ public class AuthContextServiceTest { @Test public void testAnonymousModeGetsReadOnlyOperationsCapabilities() { Fixture fixture = new Fixture(true); - Mockito.when(fixture.config.get(HubbleOptions.AUTH_ENABLED)) - .thenReturn(false); + Mockito.when(fixture.authMode.anonymous()).thenReturn(true); Map<String, Object> context = fixture.service.context(fixture.client, null); @@ -341,6 +340,8 @@ public class AuthContextServiceTest { private final HugeClient client = Mockito.mock(HugeClient.class); private final HugeConfig config = Mockito.mock(HugeConfig.class); private final UserService users = Mockito.mock(UserService.class); + private final AuthModeService authMode = + Mockito.mock(AuthModeService.class); private final AuthContextService service; private Fixture(boolean pdEnabled) { @@ -348,7 +349,8 @@ public class AuthContextServiceTest { .thenReturn(pdEnabled); Mockito.when(this.client.supportsPersonalProfileUpdate()) .thenReturn(true); - this.service = new AuthContextService(this.config, this.users); + this.service = new AuthContextService(this.config, this.users, + this.authMode); } } } diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthModeServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthModeServiceTest.java new file mode 100644 index 000000000..79a53cf92 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthModeServiceTest.java @@ -0,0 +1,55 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.service.auth; + +import org.apache.hugegraph.driver.HugeClient; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class AuthModeServiceTest { + + @Test + public void testUsesDetectedServerAuthState() { + HugeClient client = Mockito.mock(HugeClient.class); + Mockito.when(client.isServerAuthEnabled()).thenReturn(false); + AuthModeService service = new AuthModeService(null) { + @Override + protected HugeClient createUnauthClient() { + return client; + } + }; + + Assert.assertFalse(service.enabled()); + Assert.assertTrue(service.anonymous()); + Mockito.verify(client).isServerAuthEnabled(); + } + + @Test + public void testFailsClosedWhenServerCannotBeProbed() { + AuthModeService service = new AuthModeService(null) { + @Override + protected HugeClient createUnauthClient() { + throw new IllegalStateException("unavailable"); + } + }; + + Assert.assertTrue(service.enabled()); + } +} 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 34594952d..86b837b5e 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 @@ -187,10 +187,8 @@ public class AuthSecurityTest { @Test public void testAnonymousModeBlocksAuthManagementButAllowsContext() { LoginInterceptor interceptor = new LoginInterceptor(); - HugeConfig config = Mockito.mock(HugeConfig.class); - Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)) - .thenReturn(false); - AuthModeService mode = new AuthModeService(config); + AuthModeService mode = Mockito.mock(AuthModeService.class); + Mockito.when(mode.anonymous()).thenReturn(true); ReflectionTestUtils.setField(interceptor, "authMode", mode); MockHttpServletRequest users = new MockHttpServletRequest("GET", "/api/v1.3/auth/users"); @@ -246,8 +244,9 @@ public class AuthSecurityTest { 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); + AuthModeService mode = Mockito.mock(AuthModeService.class); + Mockito.when(mode.anonymous()).thenReturn(true); GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class); HugeClient client = Mockito.mock(HugeClient.class); Mockito.when(spaces.requirePublicSpace(client, "protected")) @@ -255,7 +254,7 @@ public class AuthSecurityTest { "unavailable")); ReflectionTestUtils.setField(controller, "config", config); ReflectionTestUtils.setField(controller, "authMode", - new AuthModeService(config)); + mode); ReflectionTestUtils.setField(controller, "graphSpaceAccessService", spaces); @@ -396,6 +395,9 @@ public class AuthSecurityTest { "/api/v1.3/graphspaces/space1"); request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + request.getSession().setAttribute(Constant.PASSWORD_KEY, "secret"); + request.getSession().setAttribute(Constant.PASSWORD_EXPIRE_AT_KEY, + System.currentTimeMillis() + 10000L); Assert.assertTrue(interceptor.preHandle(request, new MockHttpServletResponse(), @@ -451,6 +453,27 @@ public class AuthSecurityTest { Mockito.verify(spaces).requireAccessibleSpace(null, "space1"); } + @Test + public void testCustomInterceptorKeepsBearerForRestWithLegacyPassword() + throws Exception { + TestCustomInterceptor interceptor = new TestCustomInterceptor(); + MockHttpServletRequest request = new MockHttpServletRequest( + "GET", + "/api/v1.3/auth/users/getpersonal"); + request.getSession().setAttribute(Constant.TOKEN_KEY, "token"); + request.getSession().setAttribute(Constant.USERNAME_KEY, "admin"); + request.getSession().setAttribute(Constant.PASSWORD_KEY, "secret"); + request.getSession().setAttribute(Constant.PASSWORD_EXPIRE_AT_KEY, + System.currentTimeMillis() + 10000L); + + Assert.assertTrue(interceptor.preHandle(request, + new MockHttpServletResponse(), + null)); + + Assert.assertEquals(1, interceptor.authClients); + Assert.assertEquals("token", interceptor.token); + } + @Test public void testCustomInterceptorKeepsGraphCollectionActionsUnscoped() throws Exception { @@ -513,11 +536,10 @@ public class AuthSecurityTest { public void testAnonymousClientUsesGraphSpaceScope() 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); - AuthModeService mode = new AuthModeService(config); + AuthModeService mode = Mockito.mock(AuthModeService.class); + Mockito.when(mode.anonymous()).thenReturn(true); ReflectionTestUtils.setField(interceptor, "authMode", mode); ReflectionTestUtils.setField(interceptor, "config", config); GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class); @@ -537,12 +559,12 @@ public class AuthSecurityTest { 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); + AuthModeService mode = Mockito.mock(AuthModeService.class); + Mockito.when(mode.anonymous()).thenReturn(true); ReflectionTestUtils.setField(interceptor, "authMode", - new AuthModeService(config)); + mode); ReflectionTestUtils.setField(interceptor, "config", config); GraphSpaceService spaces = Mockito.mock(GraphSpaceService.class); Mockito.doThrow(new ExternalException(HttpStatus.NOT_FOUND.value(), @@ -745,6 +767,9 @@ public class AuthSecurityTest { Assert.assertNull(request.getSession().getAttribute(Constant.TOKEN_KEY)); Assert.assertNull(request.getSession().getAttribute(Constant.USERNAME_KEY)); + Assert.assertNull(request.getSession().getAttribute(Constant.PASSWORD_KEY)); + Assert.assertNull(request.getSession().getAttribute( + Constant.PASSWORD_EXPIRE_AT_KEY)); } @Test @@ -814,9 +839,11 @@ public class AuthSecurityTest { Constant.USERNAME_KEY)); Assert.assertEquals("server-token", request.getSession().getAttribute( Constant.TOKEN_KEY)); - Assert.assertNull(request.getSession().getAttribute("auth_password")); - Assert.assertNull(request.getSession().getAttribute( - "auth_password_expire_at")); + Assert.assertEquals("pa", request.getSession().getAttribute( + Constant.PASSWORD_KEY)); + Assert.assertTrue(((Number) request.getSession().getAttribute( + Constant.PASSWORD_EXPIRE_AT_KEY)).longValue() > + System.currentTimeMillis()); } @Test diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java index 1e5a7c6a3..039321d01 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/BaseControllerGremlinClientTest.java @@ -38,8 +38,9 @@ public class BaseControllerGremlinClientTest { } @Test - public void testGremlinClientIgnoresLegacyPasswordAndUsesToken() { + public void testGremlinClientUsesLegacyBasicCredentials() { HugeClient tokenClient = Mockito.mock(HugeClient.class); + HugeClient basicClient = Mockito.mock(HugeClient.class); MockHttpServletRequest request = this.requestWithAuth(); request.setAttribute("hugeClient", tokenClient); @@ -48,15 +49,14 @@ public class BaseControllerGremlinClientTest { TestController controller = new TestController(); controller.authClient = tokenClient; + controller.basicClient = basicClient; HugeClient client = controller.gremlinClient("DEFAULT", "hugegraph"); - Assert.assertSame(tokenClient, client); - Assert.assertSame(tokenClient, request.getAttribute("hugeClient")); - Assert.assertTrue(controller.authClientCreated); - Assert.assertEquals("DEFAULT", controller.graphSpace); - Assert.assertEquals("hugegraph", controller.graph); - Mockito.verify(tokenClient, Mockito.never()).close(); + Assert.assertSame(basicClient, client); + Assert.assertSame(basicClient, request.getAttribute("hugeClient")); + Assert.assertFalse(controller.authClientCreated); + Mockito.verify(tokenClient).close(); } @Test @@ -132,6 +132,7 @@ public class BaseControllerGremlinClientTest { private static class TestController extends BaseController { private HugeClient authClient; + private HugeClient basicClient; private boolean authClientCreated; private String graphSpace; private String graph; @@ -152,5 +153,17 @@ public class BaseControllerGremlinClientTest { this.getRequest().setAttribute("hugeClient", this.authClient); return this.authClient; } + + @Override + protected HugeClient createBasicClient(String graphSpace, String graph, + String username, String password) { + return this.basicClient; + } + + @Override + protected void requireGraphSpaceAccess(HugeClient client, + String graphSpace) { + // No graph-space service is needed for this client-selection test. + } } } 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 a60c457d9..e99ed7393 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 @@ -27,7 +27,10 @@ import org.springframework.test.util.ReflectionTestUtils; import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.controller.ConfigController; +import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.options.HubbleOptions; +import org.apache.hugegraph.service.HugeClientPoolService; +import org.apache.hugegraph.service.auth.AuthModeService; public class ConfigControllerTest { @@ -35,15 +38,84 @@ public class ConfigControllerTest { 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); + HugeClient client = Mockito.mock(HugeClient.class); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + Mockito.when(client.isServerAuthEnabled()).thenReturn(false); + Mockito.when(authMode.update(false)).thenReturn(false); - ConfigController controller = new ConfigController(); + ConfigController controller = new ConfigController() { + @Override + protected HugeClient createUnauthClient() { + return client; + } + }; ReflectionTestUtils.setField(controller, "config", config); + ReflectionTestUtils.setField(controller, "hugeClientPoolService", + new HugeClientPoolService()); + ReflectionTestUtils.setField(controller, "authModeService", authMode); Map<String, Object> result = controller.getConfig(); Assert.assertEquals(Map.of("pd_enabled", false, - "auth_enabled", true), result); + "auth_enabled", false, + "graph_create_enabled", false, + "cypher_enabled", false), result); Mockito.verify(config, Mockito.never()).get(HubbleOptions.SERVER_URL); + Mockito.verify(client).close(); + } + + @Test + public void testStandaloneConfigExposesServerCapabilities() { + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(false); + HugeClient client = Mockito.mock(HugeClient.class); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + Mockito.when(client.isServerAuthEnabled()).thenReturn(true); + Mockito.when(authMode.update(true)).thenReturn(true); + Mockito.when(client.supportsGraphCreate()).thenReturn(true); + Mockito.when(client.supportsCypher()).thenReturn(false); + + ConfigController controller = new ConfigController() { + @Override + protected HugeClient createUnauthClient() { + return client; + } + }; + ReflectionTestUtils.setField(controller, "config", config); + ReflectionTestUtils.setField(controller, "hugeClientPoolService", + new HugeClientPoolService()); + ReflectionTestUtils.setField(controller, "authModeService", authMode); + + Map<String, Object> result = controller.getConfig(); + + Assert.assertEquals(Map.of("pd_enabled", false, + "auth_enabled", true, + "graph_create_enabled", true, + "cypher_enabled", false), result); + Mockito.verify(client).close(); + } + + @Test + public void testStandaloneConfigSurvivesCapabilityProbeFailure() { + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(config.get(HubbleOptions.PD_ENABLED)).thenReturn(false); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + ConfigController controller = new ConfigController() { + @Override + protected HugeClient createUnauthClient() { + throw new IllegalStateException("server unavailable"); + } + }; + ReflectionTestUtils.setField(controller, "config", config); + ReflectionTestUtils.setField(controller, "hugeClientPoolService", + new HugeClientPoolService()); + ReflectionTestUtils.setField(controller, "authModeService", authMode); + + Map<String, Object> result = controller.getConfig(); + + Assert.assertEquals(Map.of("pd_enabled", false, + "auth_enabled", true, + "graph_create_enabled", false, + "cypher_enabled", false), result); } } diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OperationsControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OperationsControllerTest.java index 6f483ab48..83fe739de 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OperationsControllerTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/OperationsControllerTest.java @@ -122,9 +122,8 @@ public class OperationsControllerTest { OperationsDataService.class); OperationsController controller = new OperationsController(); HugeConfig config = Mockito.mock(HugeConfig.class); - Mockito.when(config.get(HubbleOptions.AUTH_ENABLED)) - .thenReturn(!anonymous); - AuthModeService authMode = new AuthModeService(config); + AuthModeService authMode = Mockito.mock(AuthModeService.class); + Mockito.when(authMode.anonymous()).thenReturn(anonymous); ReflectionTestUtils.setField(controller, "authMode", authMode); ReflectionTestUtils.setField(controller, "userService", userService); ReflectionTestUtils.setField(controller, "dataService", dataService); diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java index f2aca19e4..9ce4fe4e5 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -26,10 +26,11 @@ import org.apache.hugegraph.controller.langchain.LangChainControllerSecurityTest import org.apache.hugegraph.controller.schema.SchemaControllerSecurityTest; import org.apache.hugegraph.controller.space.GraphSpaceControllerTest; import org.apache.hugegraph.controller.space.SchemaTemplateControllerSecurityTest; -import org.apache.hugegraph.config.HubbleConfigEnvironmentTest; import org.apache.hugegraph.handler.ResponseAdvisorStatusTest; import org.apache.hugegraph.service.load.IngestTransactionIntegrationTest; import org.apache.hugegraph.service.auth.AuthContextServiceTest; +import org.apache.hugegraph.service.auth.AuthModeServiceTest; +import org.apache.hugegraph.service.auth.GraphSpaceUserServiceTest; import org.apache.hugegraph.service.space.GraphSpaceServiceTest; import org.apache.hugegraph.service.op.DefaultOperationsDataServiceTest; import org.apache.hugegraph.service.op.LiveOperationsCollectorTest; @@ -44,12 +45,14 @@ import org.junit.runners.Suite; @Suite.SuiteClasses({ AccountMutationAuthorizationTest.class, AuthContextServiceTest.class, + AuthModeServiceTest.class, AuthSecurityTest.class, AppTypeTest.class, AuthzRouteRegistrationTest.class, BusinessAssertTest.class, BaseControllerGremlinClientTest.class, ConsolePrintTest.class, + ConfigControllerTest.class, EmptyCatchTest.class, FileMappingSchemaTest.class, FileUploadControllerTest.class, @@ -61,10 +64,10 @@ import org.junit.runners.Suite; GraphSpaceAuthMutationAuthorizationTest.class, GraphSpaceAuthOwnershipTest.class, GraphSpaceServiceTest.class, + GraphSpaceUserServiceTest.class, GraphsControllerCanonicalTest.class, GremlinUtilTest.class, GremlinHistoryFailureTest.class, - HubbleConfigEnvironmentTest.class, HubbleOptionsTest.class, IngestControllerTest.class, IngestTransactionIntegrationTest.class, diff --git a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties index 442560763..8dfce484c 100644 --- a/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties +++ b/hugegraph-hubble/hubble-dist/assembly/static/conf/hugegraph-hubble.properties @@ -28,9 +28,6 @@ idc=bddwd client.url_cache_max_entries=1024 # ===== Deployment Mode ===== -# Require an authenticated Hubble session. HUBBLE_AUTH_ENABLED can explicitly -# override this value for container and orchestrated deployments. -auth.enabled=true # Set to false for standalone RocksDB mode (no PD dependency) pd.enabled=false # Direct server URL, only used when pd.enabled=false diff --git a/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh b/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh index df4abd4e2..b1e93be0e 100755 --- a/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh +++ b/hugegraph-hubble/hubble-dist/assembly/travis/download-hugegraph.sh @@ -25,7 +25,7 @@ fi COMMIT_ID=$1 COMMIT_REF=${2:-} -HUGEGRAPH_GIT_URL="https://github.com/apache/hugegraph.git" +HUGEGRAPH_GIT_URL=${HUGEGRAPH_GIT_URL:-"https://github.com/apache/hugegraph.git"} GIT_DIR=hugegraph CACHE_DIR="${HOME}/hugegraph-cache-${COMMIT_ID}" diff --git a/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.js b/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.js index deadbf473..88567abae 100644 --- a/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.js +++ b/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.js @@ -29,11 +29,15 @@ const resolveGraphspaceAccess = (context, graphspace, pdEnabled) => { const scopes = context.scopes ?? {}; const anonymousStandalone = context.mode === 'NON_AUTH' && !pdEnabled; + const anonymousPd = context.mode === 'NON_AUTH' && pdEnabled; const canManage = anonymousStandalone - || context.role === 'SUPERADMIN' - || scopes.all_graphspaces === true - || includes(scopes.admin_graphspaces, graphspace); + || (!anonymousPd && ( + context.role === 'SUPERADMIN' + || scopes.all_graphspaces === true + || includes(scopes.admin_graphspaces, graphspace) + )); const canWrite = canManage + || anonymousPd || context.mode === 'NON_PD' || includes(scopes.write_graphspaces, graphspace); return {canManage, canWrite}; diff --git a/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.test.js b/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.test.js index bf2cff573..86652aa59 100644 --- a/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.test.js +++ b/hugegraph-hubble/hubble-fe/src/auth/graphspaceAccess.test.js @@ -33,6 +33,11 @@ test.each([ }, true, true, true], [{mode: 'NON_PD', role: 'USER', scopes: {}}, false, false, true], [{mode: 'NON_PD', role: 'SUPERADMIN', scopes: {}}, false, true, true], + [{ + mode: 'NON_AUTH', + role: 'ANONYMOUS', + scopes: {all_graphspaces: true}, + }, true, false, true], [{mode: 'NON_AUTH', role: 'ANONYMOUS', scopes: {}}, false, true, true], ])( 'resolves graphspace access for %#', diff --git a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js index 92ae8b41b..bdb628edc 100644 --- a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js +++ b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.js @@ -64,11 +64,14 @@ const getRecords = response => { return response.data?.records ?? []; }; -const errorKind = error => { - const status = error?.status ?? error?.response?.data?.status - ?? error?.response?.status; - return status === 403 ? 'forbidden' : true; -}; +const errorStatus = error => error?.status ?? error?.response?.data?.status + ?? error?.response?.status; + +const errorKind = error => ( + errorStatus(error) === 403 ? 'forbidden' : true +); + +const unavailableGraphSpace = error => [403, 404].includes(errorStatus(error)); const GraphContextSwitcher = () => { const {t} = useTranslation(); @@ -157,7 +160,7 @@ const GraphContextSwitcher = () => { .catch(error => { if (!cancelled) { const kind = errorKind(error); - if (pdEnabled && kind === 'forbidden') { + if (pdEnabled && unavailableGraphSpace(error)) { clearWorkbenchGraphContext(localStorage); setContext({}); setGraphs([]); diff --git a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js index 8e1d764df..4002b2ed2 100644 --- a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js +++ b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js @@ -326,6 +326,26 @@ describe('GraphContextSwitcher', () => { .not.toBeInTheDocument(); }); + test('clears a masked missing PD GraphSpace after access is revoked', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + localStorage.setItem('hubble_workbench_graph_context', JSON.stringify({ + graphspace: 'space_a', + graph: 'graph_a', + })); + api.manage.getGraphList.mockResolvedValueOnce({status: 404, data: null}); + renderSwitcher('/gremlin/space_a/graph_a'); + + await waitFor(() => { + expect(screen.getByText('/navigation')).toBeInTheDocument(); + }); + expect(localStorage.getItem('hubble_workbench_graph_context')).toBeNull(); + expect(screen.getByRole('combobox', { + name: 'workbench.context.graphspace', + })).toHaveValue(''); + expect(screen.queryByText('workbench.context.graphs_load_failed')) + .not.toBeInTheDocument(); + }); + test('graph success cannot erase a concurrent GraphSpace failure', async () => { sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); api.manage.getGraphSpaceList.mockResolvedValueOnce({status: 500, data: null}); diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js index 744fae2c0..a75b2e76a 100644 --- a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js +++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js @@ -63,6 +63,7 @@ const items = (t, pathname, capabilities = []) => { key: 'nodes', }] : []), ]; + const supportList = [...operationsList, ...systemList]; const menu = [ { @@ -125,12 +126,12 @@ const items = (t, pathname, capabilities = []) => { }, ], }, - { + ...(supportList.length > 0 ? [{ label: t('operations.section'), key: 'support', icon: <DashboardOutlined />, - children: [...operationsList, ...systemList], - }, + children: supportList, + }] : []), ]; return menu; diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js index db826c47f..2fe9f7aa8 100644 --- a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js +++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js @@ -94,6 +94,31 @@ test('hides account and profile links in anonymous mode', async () => { expect(screen.queryByRole('link', {name: '账号管理'})).not.toBeInTheDocument(); }); +test('hides an empty operations section in anonymous standalone mode', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({ + pd_enabled: false, + auth_enabled: false, + })); + useOperationsCapabilities.mockReturnValue({ + loading: false, + capabilities: [], + error: null, + }); + + render( + <MemoryRouter + initialEntries={['/navigation']} + future={{v7_startTransition: true, v7_relativeSplatPath: true}} + > + <Sidebar /> + </MemoryRouter> + ); + + expect(await screen.findByRole('navigation', {name: '主导航'})) + .toBeInTheDocument(); + expect(screen.queryByText('系统与运维')).not.toBeInTheDocument(); +}); + test('keeps monitoring and account links in one operations section', async () => { sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); render( diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json index 7547d188f..97463509a 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 @@ -186,6 +186,7 @@ "title": "Graph Management", "view_mode": "Graph list view", "create": "New Graph", + "server_upgrade_hint": "This Server version is too old for some features. Please upgrade to the latest version.", "search_placeholder": "Search by graph name", "unavailable": "Graphs are unavailable. Check the server connection and retry.", "set_default_confirm": "Confirm changing the default graph?", @@ -513,6 +514,7 @@ "new_password_placeholder": "Enter new password", "confirm_password_placeholder": "Enter new password again", "password_mismatch": "Passwords do not match", + "password_changed_relogin": "Password changed successfully. Please sign in again.", "account_name_rule": "Use 1–16 characters without spaces; letters, numbers, Chinese/CJK forms, and underscores are supported; underscores cannot be first or last" } }, 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 322f3e4ce..0861fd367 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 @@ -186,6 +186,7 @@ "title": "图管理", "view_mode": "图列表视图", "create": "新建图", + "server_upgrade_hint": "当前 Server 版本过低,部分功能不可用,请及时升级到最新版本。", "search_placeholder": "请输入图名称", "unavailable": "图列表暂不可用,请检查 Server 连接后重试。", "set_default_confirm": "确认更改图的默认设置?", @@ -513,6 +514,7 @@ "new_password_placeholder": "请输入新密码", "confirm_password_placeholder": "请再次输入密码", "password_mismatch": "两次密码不一致", + "password_changed_relogin": "密码修改成功,请重新登录。", "account_name_rule": "使用 1–16 个不含空格的字符,支持中文/东亚兼容字符、字母、数字和下划线;下划线不能位于首尾" } }, diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js index dec24f9ba..32b60df0b 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/Home/index.js @@ -32,6 +32,7 @@ import * as api from '../../../api'; import _ from 'lodash'; import {scopedStorageKey} from '../../../utils/user'; import {sanitizePublicError} from '../../../utils/publicError'; +import {isCypherEnabled} from '../../../utils/config'; const {STANDBY, LOADING, SUCCESS, FAILED} = GRAPH_STATUS; const {QUERY} = GREMLIN_EXECUTES_MODE; @@ -75,6 +76,7 @@ export const extractQueryErrorMessage = (error, fallback) => { const AnalysisHome = () => { const {t} = useTranslation(); const {graphSpace, graph} = useContext(GraphAnalysisContext); + const cypherEnabled = isCypherEnabled(); const [queryStatus, setQueryStatus] = useState(STANDBY); const [queryMessage, setQueryMessage] = useState(); const [isQueryMode, setQueryMode] = useState(true); @@ -567,6 +569,7 @@ const AnalysisHome = () => { onExecute={onExecute} onRefresh={onFavoriteRefresh} isExecuting={queryStatus === LOADING} + cypherEnabled={cypherEnabled} /> {analysisMode !== TEXT2GQL && <QueryResult queryResult={queryResult} diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.js index 44850e7d5..9d9281aba 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.js @@ -42,6 +42,7 @@ const QueryBar = props => { onTabsChange, onExecute, isExecuting, + cypherEnabled = false, } = args; const [isEmptyQuery, setIsEmptyQuery] = useState(() => !codeEditorContent); @@ -132,11 +133,11 @@ const QueryBar = props => { key: GREMLIN, children: renderEditor('gremlin'), }, - { + ...(cypherEnabled ? [{ label: t('analysis.query.cypher_tab'), key: CYPHER, children: renderEditor('cypher'), - }, + }] : []), { label: ( <span> diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.test.js b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.test.js index e3a6027d8..106d55d95 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.test.js +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/Home/index.test.js @@ -109,12 +109,26 @@ it('shows a same-level Text2GQL preview with no executable control', () => { expect(screen.queryByRole('button', {name: /run|execute/i})).not.toBeInTheDocument(); }); +it('hides Cypher when capability is absent', () => { + render( + <QueryBar + activeTab='Gremlin' + onTabsChange={jest.fn()} + codeEditorContent='' + setCodeEditorContent={jest.fn()} + /> + ); + + expect(screen.queryByRole('tab', {name: 'Cypher'})).not.toBeInTheDocument(); +}); + it('does not transfer an open favorite popover when query tabs change', () => { const ControlledQueryBar = () => { const [activeTab, setActiveTab] = useState('Gremlin'); return ( <QueryBar activeTab={activeTab} + cypherEnabled onTabsChange={setActiveTab} codeEditorContent='g.V()' setCodeEditorContent={jest.fn()} @@ -137,6 +151,7 @@ it('matches the editor placeholder to the active query language without promotio return ( <QueryBar activeTab={activeTab} + cypherEnabled onTabsChange={setActiveTab} codeEditorContent='' setCodeEditorContent={jest.fn()} diff --git a/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js b/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js index 9507133e4..f3bf49228 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js @@ -96,7 +96,10 @@ beforeAll(installMatchMedia); beforeEach(() => { jest.clearAllMocks(); installMatchMedia(); - sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + sessionStorage.setItem('hubble_config_', JSON.stringify({ + pd_enabled: true, + graph_create_enabled: true, + })); mockAuthContext = { context: {role: 'SUPERADMIN', scopes: {all_graphspaces: true}}, }; diff --git a/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js b/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js index 60d634f5b..8d7732033 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js @@ -42,10 +42,9 @@ import {PlusOutlined} from '@ant-design/icons'; import {Link, useParams, useNavigate} from 'react-router-dom'; import style from './index.module.scss'; import * as api from '../../api'; -import {isPdEnabled} from '../../utils/config'; +import {isGraphCreateEnabled, isPdEnabled} from '../../utils/config'; import { DEFAULT_GRAPHSPACE, - isGraphCreateEnabled, isGraphDefaultMutationEnabled, } from '../../utils/productMode'; import moment from 'moment'; @@ -107,7 +106,10 @@ const Graph = () => { const {graphspace} = useParams(); const navigate = useNavigate(); const pdMode = isPdEnabled(); - const graphCreateEnabled = isGraphCreateEnabled(pdMode); + const graphCreateEnabled = isGraphCreateEnabled(); + const graphCreateUnavailableHint = t('graph.server_upgrade_hint'); + const cloneUnavailableHint = graphCreateEnabled + ? t('graph.clone.unavailable') : graphCreateUnavailableHint; const graphDefaultMutationEnabled = isGraphDefaultMutationEnabled(pdMode); const {canManage, canWrite} = useGraphspaceAccess(graphspace); @@ -435,19 +437,17 @@ const Graph = () => { </GraphRowAction> ) )} - {graphCreateEnabled && ( - <Tooltip title={t('graph.clone.unavailable')}> - <span - className={style.disable} - role='button' - aria-disabled='true' - aria-label={`${t('graph.menu.clone')}: ${t('graph.clone.unavailable')}`} - tabIndex={0} - > - {t('graph.menu.clone')} - </span> - </Tooltip> - )} + <Tooltip title={cloneUnavailableHint}> + <span + className={style.disable} + role='button' + aria-disabled='true' + aria-label={`${t('graph.menu.clone')}: ${cloneUnavailableHint}`} + tabIndex={0} + > + {t('graph.menu.clone')} + </span> + </Tooltip> </Space> ); }, @@ -518,13 +518,13 @@ const Graph = () => { onClick: immutable || !canManage ? undefined : () => deleteGraph(item.name), }, - graphCreateEnabled && { + { key: 'clone', disabled: true, label: ( - <Tooltip title={t('graph.clone.unavailable')}> + <Tooltip title={cloneUnavailableHint}> <span - aria-label={`${t('graph.menu.clone')}: ${t('graph.clone.unavailable')}`} + aria-label={`${t('graph.menu.clone')}: ${cloneUnavailableHint}`} > {t('graph.menu.clone')} </span> @@ -614,6 +614,13 @@ const Graph = () => { )} /> )} + {!graphCreateEnabled && ( + <Alert + showIcon + type='info' + message={graphCreateUnavailableHint} + /> + )} {listType === 'image' ? ( <> diff --git a/hugegraph-hubble/hubble-fe/src/pages/My/index.js b/hugegraph-hubble/hubble-fe/src/pages/My/index.js index cad01a205..f1b786774 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/My/index.js +++ b/hugegraph-hubble/hubble-fe/src/pages/My/index.js @@ -24,7 +24,11 @@ import style from './index.module.scss'; import EditLayer from './EditLayer'; import * as api from '../../api'; import * as rules from '../../utils/rules'; +import * as user from '../../utils/user'; import {useAuthContext} from '../../auth/AuthContext'; +import { + clearPersistedAlgorithmFormsForUser, +} from '../../modules/algorithm/algorithmsForm/algorithmFormPersistence'; const My = () => { const {t} = useTranslation(); @@ -76,8 +80,12 @@ const My = () => { setLoading(true); const res = await api.auth.updatePwd(data.user_name, old_password, user_password); if (res.status === 200) { - message.success(t('common.msg.update_success')); - setChangePass(false); + message.success(t('my.edit.password_changed_relogin')); + user.beginLogoutTransition(); + sessionStorage.removeItem('redirect'); + clearPersistedAlgorithmFormsForUser(); + user.clearLogin(); + window.location.replace('/login'); return; } diff --git a/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js b/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js index a530bbe62..d7b7c6042 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/My/my-recovery.test.js @@ -20,6 +20,10 @@ import {render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import My from './index'; import * as api from '../../api'; +import * as user from '../../utils/user'; +import { + clearPersistedAlgorithmFormsForUser, +} from '../../modules/algorithm/algorithmsForm/algorithmFormPersistence'; let mockAuthContext = null; @@ -39,6 +43,15 @@ jest.mock('../../api', () => ({ }, })); +jest.mock('../../utils/user', () => ({ + clearLogin: jest.fn(), + beginLogoutTransition: jest.fn(), +})); + +jest.mock('../../modules/algorithm/algorithmsForm/algorithmFormPersistence', () => ({ + clearPersistedAlgorithmFormsForUser: jest.fn(), +})); + jest.mock('./EditLayer', () => ({refresh}) => ( <button type='button' onClick={refresh}>mock profile saved</button> )); @@ -60,6 +73,9 @@ beforeEach(() => { addListener: jest.fn(), removeListener: jest.fn(), })); + sessionStorage.clear(); + delete window.location; + window.location = {replace: jest.fn()}; api.auth.status.mockResolvedValue({status: 200, data: {level: 'ADMIN'}}); }); @@ -232,6 +248,8 @@ test('stops password submit loading when form validation rejects', async () => { await waitFor(() => expect(document.querySelector('.ant-form-item-has-error')).not.toBeNull()); await waitFor(() => expect(confirm).not.toHaveClass('ant-btn-loading')); expect(api.auth.updatePwd).not.toHaveBeenCalled(); + expect(user.clearLogin).not.toHaveBeenCalled(); + expect(window.location.replace).not.toHaveBeenCalled(); expect(screen.getByPlaceholderText('my.edit.new_password_placeholder')).toBeInTheDocument(); }); @@ -247,6 +265,8 @@ test('stops password submit loading when the request rejects', async () => { )); await waitFor(() => expect(confirm).not.toHaveClass('ant-btn-loading')); expect(screen.getByPlaceholderText('my.edit.new_password_placeholder')).toBeInTheDocument(); + expect(user.clearLogin).not.toHaveBeenCalled(); + expect(window.location.replace).not.toHaveBeenCalled(); }); test('stops password submit loading and preserves the form on a non-200 response', async () => { @@ -261,11 +281,14 @@ test('stops password submit loading and preserves the form on a non-200 response )); await waitFor(() => expect(confirm).not.toHaveClass('ant-btn-loading')); expect(screen.getByPlaceholderText('my.edit.old_password_placeholder')).toHaveValue('old-pass'); + expect(user.clearLogin).not.toHaveBeenCalled(); + expect(window.location.replace).not.toHaveBeenCalled(); }); -test('keeps loading during a password request and closes the form only on success', async () => { +test('clears local auth and redirects only after the password update succeeds', async () => { const request = deferred(); api.auth.updatePwd.mockReturnValue(request.promise); + sessionStorage.setItem('redirect', '/gremlin/DEFAULT/hugegraph'); const confirm = await openPasswordForm(); await fillValidPasswords(); @@ -278,8 +301,9 @@ test('keeps loading during a password request and closes the form only on succes request.resolve({status: 200}); - await waitFor(() => expect( - screen.queryByPlaceholderText('my.edit.new_password_placeholder') - ).not.toBeInTheDocument()); - expect(screen.getByRole('heading', {name: 'Administrator'})).toBeInTheDocument(); + await waitFor(() => expect(user.clearLogin).toHaveBeenCalledTimes(1)); + expect(user.beginLogoutTransition).toHaveBeenCalledTimes(1); + expect(clearPersistedAlgorithmFormsForUser).toHaveBeenCalledTimes(1); + expect(sessionStorage.getItem('redirect')).toBeNull(); + expect(window.location.replace).toHaveBeenCalledWith('/login'); }); diff --git a/hugegraph-hubble/hubble-fe/src/utils/config.js b/hugegraph-hubble/hubble-fe/src/utils/config.js index e17acbf93..0409bbcaf 100644 --- a/hugegraph-hubble/hubble-fe/src/utils/config.js +++ b/hugegraph-hubble/hubble-fe/src/utils/config.js @@ -35,4 +35,19 @@ const isAuthEnabled = () => { return getConfig().auth_enabled !== false; }; -export {setConfig, getConfig, isPdEnabled, isAuthEnabled}; +const isGraphCreateEnabled = () => { + return getConfig().graph_create_enabled === true; +}; + +const isCypherEnabled = () => { + return getConfig().cypher_enabled === true; +}; + +export { + setConfig, + getConfig, + isPdEnabled, + isAuthEnabled, + isGraphCreateEnabled, + isCypherEnabled, +}; diff --git a/hugegraph-hubble/hubble-fe/src/utils/config.js b/hugegraph-hubble/hubble-fe/src/utils/config.test.js similarity index 60% copy from hugegraph-hubble/hubble-fe/src/utils/config.js copy to hugegraph-hubble/hubble-fe/src/utils/config.test.js index e17acbf93..0a6ff2448 100644 --- a/hugegraph-hubble/hubble-fe/src/utils/config.js +++ b/hugegraph-hubble/hubble-fe/src/utils/config.test.js @@ -16,23 +16,27 @@ * under the License. */ -const CONFIG_KEY = 'hubble_config_'; +import { + isCypherEnabled, + isGraphCreateEnabled, + setConfig, +} from './config'; -const setConfig = config => { - sessionStorage.setItem(CONFIG_KEY, JSON.stringify(config)); -}; +beforeEach(() => { + sessionStorage.clear(); +}); -const getConfig = () => { - const str = sessionStorage.getItem(CONFIG_KEY); - return str ? JSON.parse(str) : {pd_enabled: true}; -}; +test('keeps unknown server capabilities disabled', () => { + expect(isGraphCreateEnabled()).toBe(false); + expect(isCypherEnabled()).toBe(false); +}); -const isPdEnabled = () => { - return getConfig().pd_enabled; -}; +test('uses explicit server capability flags', () => { + setConfig({ + graph_create_enabled: true, + cypher_enabled: false, + }); -const isAuthEnabled = () => { - return getConfig().auth_enabled !== false; -}; - -export {setConfig, getConfig, isPdEnabled, isAuthEnabled}; + expect(isGraphCreateEnabled()).toBe(true); + expect(isCypherEnabled()).toBe(false); +}); diff --git a/hugegraph-hubble/hubble-fe/src/utils/productMode.js b/hugegraph-hubble/hubble-fe/src/utils/productMode.js index 9f0cbdf0f..a9e971a2d 100644 --- a/hugegraph-hubble/hubble-fe/src/utils/productMode.js +++ b/hugegraph-hubble/hubble-fe/src/utils/productMode.js @@ -65,8 +65,6 @@ const getTaskGraphspaceOptions = (pdEnabled, graphspaces = []) => { })); }; -const isGraphCreateEnabled = () => true; - const isGraphDefaultMutationEnabled = pdEnabled => pdEnabled; export { @@ -74,7 +72,6 @@ export { getGraphspacePath, getManageNavItems, getTaskGraphspaceOptions, - isGraphCreateEnabled, isGraphDefaultMutationEnabled, isPdOnlyPath, shouldUseNonPdDefaultGraphspace, diff --git a/hugegraph-hubble/hubble-fe/src/utils/productMode.test.js b/hugegraph-hubble/hubble-fe/src/utils/productMode.test.js index b928d7152..544a6b5cd 100644 --- a/hugegraph-hubble/hubble-fe/src/utils/productMode.test.js +++ b/hugegraph-hubble/hubble-fe/src/utils/productMode.test.js @@ -20,7 +20,6 @@ import { getGraphspacePath, getManageNavItems, getTaskGraphspaceOptions, - isGraphCreateEnabled, isGraphDefaultMutationEnabled, isPdOnlyPath, shouldUseNonPdDefaultGraphspace, @@ -55,9 +54,7 @@ describe('product mode helpers', () => { expect(shouldUseNonPdDefaultGraphspace(true, 'demo')).toBe(false); }); - test('keeps graph create available while disabling default mutation in non-PD mode', () => { - expect(isGraphCreateEnabled(false)).toBe(true); - expect(isGraphCreateEnabled(true)).toBe(true); + test('keeps default mutation scoped to PD mode', () => { expect(isGraphDefaultMutationEnabled(false)).toBe(false); expect(isGraphDefaultMutationEnabled(true)).toBe(true); });
