From 1fd9a215ac4d034f7420cf00409fc029f218969f Mon Sep 17 00:00:00 2001 From: arshil-khan Date: Thu, 8 Oct 2026 00:12:06 +0530 Subject: [PATCH 1/6] fix(server): decode Basic auth as UTF-8, split once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Decode the Basic credential as UTF-8 (RFC 7617 section 2.1). US-ASCII turned every non-ASCII byte into U+FFFD, so a password such as "ädminpass1" never matched the stored hash and the request got 401. - Split at the first colon. RFC 7617 bars a colon from the user-id only, and split(":") answered 400 for a password such as "new:pass1234". - A credential with no colon, an empty user-id or an empty password still gets 400. - Add a LoginApiTest case covering both passwords. --- .../api/filter/AuthenticationFilter.java | 19 +++++----- .../apache/hugegraph/api/LoginApiTest.java | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java index 13bb05d8d9..8e09637242 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java @@ -20,6 +20,7 @@ import static org.apache.hugegraph.config.ServerOptions.WHITE_IP_STATUS; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.Principal; import java.util.HashMap; import java.util.List; @@ -39,7 +40,6 @@ import org.apache.hugegraph.util.Log; import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException; import org.glassfish.grizzly.http.server.Request; -import org.glassfish.grizzly.utils.Charsets; import org.gridkit.jvmtool.cmd.AntPathMatcher; import org.slf4j.Logger; @@ -184,19 +184,16 @@ protected User authenticate(ContainerRequestContext context) { if (auth.startsWith(BASIC_AUTH_PREFIX)) { auth = auth.substring(BASIC_AUTH_PREFIX.length()); - // TODO: decode the Basic credential as UTF-8 and split it on the first colon only. - // Decoding as ASCII and splitting on every colon makes a non-ASCII password answer - // 401 and a password containing ':' answer 400, although both were accepted at - // account creation. The Helm chart (helm/hugegraph) refuses such admin passwords in - // its schema and Server wrapper; drop that guard once this is fixed. - auth = new String(DatatypeConverter.parseBase64Binary(auth), Charsets.ASCII_CHARSET); - String[] values = auth.split(":"); - if (values.length != 2) { + // RFC 7617: the credential is UTF-8, and only the user-id is barred from + // containing a colon, so split on the first one and keep the rest as the password + auth = new String(DatatypeConverter.parseBase64Binary(auth), StandardCharsets.UTF_8); + int colon = auth.indexOf(':'); + if (colon < 0) { throw new BadRequestException("Invalid syntax for username and password"); } - final String username = values[0]; - final String password = values[1]; + final String username = auth.substring(0, colon); + final String password = auth.substring(colon + 1); if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { throw new BadRequestException("Invalid syntax for username and password"); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java index 0b6df93438..c981de8475 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java @@ -17,7 +17,9 @@ package org.apache.hugegraph.api; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; +import java.util.Base64; import java.util.Map; import org.apache.hugegraph.testutil.Assert; @@ -136,6 +138,40 @@ public void testVerify() { assertResponseStatus(401, r); } + @Test + public void testBasicAuthWithNonAsciiOrColonPassword() { + // RFC 7617: the credential is UTF-8 and only the first colon separates the fields + String[][] users = {{"utf8user", "\u00e4dminpass1"}, + {"coloned", "new:pass1234"}}; + RestClient noAuthClient = new RestClient(baseUrl(), false); + try { + for (String[] user : users) { + Response r = this.createUser(user[0], user[1]); + String result = assertResponseStatus(201, r); + String id = (String) JsonUtil.fromJson(result, Map.class).get("id"); + try { + r = basicAuthGet(noAuthClient, user[0], user[1]); + assertResponseStatus(200, r); + + r = basicAuthGet(noAuthClient, user[0], "wrong" + user[1]); + assertResponseStatus(401, r); + } finally { + this.deleteUser(id); + } + } + } finally { + noAuthClient.close(); + } + } + + private static Response basicAuthGet(RestClient client, String name, String password) { + byte[] credential = (name + ":" + password).getBytes(StandardCharsets.UTF_8); + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, + "Basic " + Base64.getEncoder().encodeToString(credential)); + return client.get("graphspaces/DEFAULT/graphs", headers); + } + private Response createUser(String name, String password) { String user = "{\"user_name\":\"%s\",\"user_password\":\"%s" + "\",\"user_email\":\"user1@baidu.com\"," + From 56a4e06226cc61e87fb8c8e6e018ec79618f63fb Mon Sep 17 00:00:00 2001 From: arshil-khan Date: Thu, 8 Oct 2026 00:18:13 +0530 Subject: [PATCH 2/6] fix(server): split Cypher Basic auth on first colon - CypherAPI.toUserPass still split the decoded credential on every colon and returned null for anything but two parts, so a password with ':' passed AuthenticationFilter and then failed on the cypher endpoint. It now splits once, like the filter and Gremlin's handler. - Reword the filter comment: RFC 7617 allows UTF-8 as the only declared charset and does not define a default. - Point the Helm wrapper comment at #3284; the TODO it named is gone. - Parse the created user with TypeReference like the other tests in LoginApiTest, and say why the Basic header is built by hand. --- helm/hugegraph/templates/server-deployment.yaml | 4 ++-- .../main/java/org/apache/hugegraph/api/cypher/CypherAPI.java | 2 +- .../apache/hugegraph/api/filter/AuthenticationFilter.java | 4 ++-- .../src/main/java/org/apache/hugegraph/api/LoginApiTest.java | 5 ++++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/helm/hugegraph/templates/server-deployment.yaml b/helm/hugegraph/templates/server-deployment.yaml index bfbdf16e6a..48d24ebaf9 100644 --- a/helm/hugegraph/templates/server-deployment.yaml +++ b/helm/hugegraph/templates/server-deployment.yaml @@ -146,8 +146,8 @@ spec: # 401 to every request made with it. Temporary: drop this check, # the colon case above and the matching schema pattern once the # Server decodes Basic auth as UTF-8 and splits the credential on - # its first colon only (TODO in AuthenticationFilter, - # apache/hugegraph#3260). + # its first colon only (apache/hugegraph#3284) in the image this + # chart deploys. if (( $(printf '%s' "${PASSWORD}" | LC_ALL=C tr -d '\040-\176' | wc -c) > 0 )); then echo "auth Secret key 'password' must be printable ASCII (bytes" \ "0x20-0x7E: no tabs, newlines or non-ASCII characters): the" \ diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java index a30bff73b8..fb865967ab 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java @@ -159,7 +159,7 @@ private Pair toUserPass(String auth) { String encoded = auth.substring(basic.length()); byte[] userPass = this.decoder.decode(encoded); String authorization = new String(userPass, UTF8); - split = authorization.split(":"); + split = authorization.split(":", 2); } catch (Exception e) { LOG.error("Failed convert auth to credential.", e); return null; diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java index 8e09637242..ed37c6592c 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/filter/AuthenticationFilter.java @@ -184,8 +184,8 @@ protected User authenticate(ContainerRequestContext context) { if (auth.startsWith(BASIC_AUTH_PREFIX)) { auth = auth.substring(BASIC_AUTH_PREFIX.length()); - // RFC 7617: the credential is UTF-8, and only the user-id is barred from - // containing a colon, so split on the first one and keep the rest as the password + // RFC 7617 section 2.1 allows only UTF-8 as the declared charset, and it bars a + // colon from the user-id alone, so split on the first one and keep the rest auth = new String(DatatypeConverter.parseBase64Binary(auth), StandardCharsets.UTF_8); int colon = auth.indexOf(':'); if (colon < 0) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java index c981de8475..254f7558dc 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java @@ -148,7 +148,9 @@ public void testBasicAuthWithNonAsciiOrColonPassword() { for (String[] user : users) { Response r = this.createUser(user[0], user[1]); String result = assertResponseStatus(201, r); - String id = (String) JsonUtil.fromJson(result, Map.class).get("id"); + Map created = JsonUtil.fromJson( + result, new TypeReference>() {}); + String id = (String) created.get("id"); try { r = basicAuthGet(noAuthClient, user[0], user[1]); assertResponseStatus(200, r); @@ -165,6 +167,7 @@ public void testBasicAuthWithNonAsciiOrColonPassword() { } private static Response basicAuthGet(RestClient client, String name, String password) { + // Built by hand: Jersey's HttpAuthenticationFeature encodes the credential as ISO-8859-1 byte[] credential = (name + ":" + password).getBytes(StandardCharsets.UTF_8); MultivaluedMap headers = new MultivaluedHashMap<>(); headers.add(HttpHeaders.AUTHORIZATION, From 5e5be9c99961b1650f14db3301b924db614de9b6 Mon Sep 17 00:00:00 2001 From: arshil-khan Date: Sat, 10 Oct 2026 14:19:31 +0530 Subject: [PATCH 3/6] chore(server): add Cypher colon-password auth test - CypherApiTest#testBasicAuthWithColonPassword creates a user whose password is "a:b:c", grants its group READ and EXECUTE on the graph, and sends a hand-built UTF-8 Basic header to the Cypher endpoint. It expects a successful query result, and 401 for a wrong password. - The user, group, target, belong and accesses it creates are deleted in reverse order when the test ends. - With the old split(":") in CypherAPI.toUserPass the test fails with 500 "The 'user-password-pair' can't be null". - Write the non-ASCII password in LoginApiTest as a literal; checkstyle flags Unicode escapes. --- .../apache/hugegraph/api/CypherApiTest.java | 65 +++++++++++++++++++ .../apache/hugegraph/api/LoginApiTest.java | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java index 23abf42601..3b2942bba6 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java @@ -19,6 +19,10 @@ import static org.apache.hugegraph.testutil.Assert.assertContains; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Base64; +import java.util.Deque; import java.util.List; import java.util.Map; @@ -27,8 +31,12 @@ import org.junit.Before; import org.junit.Test; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; public class CypherApiTest extends BaseApiTest { @@ -177,6 +185,63 @@ public void testReturnPathShape() { assertNoHugeGraphIdLeak(content); } + @Test + public void testBasicAuthWithColonPassword() { + // CypherAPI parses the Basic header itself, apart from AuthenticationFilter + String user = "cypher_colon_user"; + String password = "a:b:c"; + Deque created = new ArrayDeque<>(); + RestClient noAuthClient = new RestClient(baseUrl(), false); + try { + String userId = this.createAuth(created, "graphspaces/DEFAULT/auth/users", String.format( + "{\"user_name\":\"%s\",\"user_password\":\"%s\"}", user, password)); + String groupId = this.createAuth(created, "auth/groups", + "{\"group_name\":\"cypher_colon_group\"}"); + String targetId = this.createAuth(created, "graphspaces/DEFAULT/auth/targets", + "{\"target_name\":\"cypher_colon_target\"," + + "\"target_graph\":\"hugegraph\"," + + "\"target_url\":\"127.0.0.1:8080\"," + + "\"target_resources\":[{\"type\":\"ALL\"}]}"); + this.createAuth(created, "graphspaces/DEFAULT/auth/belongs", String.format( + "{\"user\":\"%s\",\"group\":\"%s\"}", userId, groupId)); + for (String permission : ImmutableList.of("READ", "EXECUTE")) { + this.createAuth(created, "graphspaces/DEFAULT/auth/accesses", String.format( + "{\"group\":\"%s\",\"target\":\"%s\",\"access_permission\":\"%s\"}", + groupId, targetId, permission)); + } + + Response r = basicAuthCypher(noAuthClient, user, password); + String content = this.validStatusAndTextContains(QUERY_RESULT, r); + Assert.assertFalse(assertCypherSuccessData(content).isEmpty()); + + r = basicAuthCypher(noAuthClient, user, "wrong" + password); + assertResponseStatus(401, r); + } finally { + noAuthClient.close(); + while (!created.isEmpty()) { + String[] entry = created.pop(); + client().delete(entry[0], entry[1]); + } + } + } + + private String createAuth(Deque created, String path, String body) { + String content = assertResponseStatus(201, client().post(path, body)); + String id = assertJsonContains(content, "id"); + created.push(new String[]{path, id}); + return id; + } + + private static Response basicAuthCypher(RestClient client, String user, String password) { + // Built by hand: Jersey's HttpAuthenticationFeature encodes the credential as ISO-8859-1 + byte[] credential = (user + ":" + password).getBytes(StandardCharsets.UTF_8); + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, + "Basic " + Base64.getEncoder().encodeToString(credential)); + return client.target().path(PATH).queryParam("cypher", QUERY) + .request().headers(headers).get(); + } + private String testCypherQueryAndContains(String cypher, String containsText) { Response r = client().post(PATH, cypher); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java index 254f7558dc..ea6d5eca82 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java @@ -141,7 +141,7 @@ public void testVerify() { @Test public void testBasicAuthWithNonAsciiOrColonPassword() { // RFC 7617: the credential is UTF-8 and only the first colon separates the fields - String[][] users = {{"utf8user", "\u00e4dminpass1"}, + String[][] users = {{"utf8user", "ädminpass1"}, {"coloned", "new:pass1234"}}; RestClient noAuthClient = new RestClient(baseUrl(), false); try { From 7e048d1ba05bca534e7de662d543921592e2deb9 Mon Sep 17 00:00:00 2001 From: arshil-khan Date: Sun, 11 Oct 2026 09:46:27 +0530 Subject: [PATCH 4/6] fix(server): decode Cypher Basic auth as standard Base64 - CypherAPI used Base64.getUrlDecoder(), which rejects '+' and '/'. A Basic header carries standard Base64 (RFC 7617, RFC 4648 section 4), so a credential that encodes to either character got 500 from the Cypher endpoint while REST accepted it. Use Base64.getDecoder(), as WsAndHttpBasicAuthHandler does. - testBasicAuthWithColonPassword now runs a:b:c, a:b:c~ and a:b:c? for cypher_colon_user. The last two encode with '+' and '/', which the test asserts so a rename cannot drop that coverage unnoticed. Each password must return the query result, and a wrong one 401. With the URL-safe decoder back in place the test fails with 500 "The 'user-password-pair' can't be null". - Skip that test outside standalone mode. Its group, target and access setup uses the standalone auth API; on HStore groups are scoped to a graphspace and creating the access gets 403 "group belongs to another graphspace", which failed the hstore api-test job. The header parsing it covers does not depend on the backend. --- .../hugegraph/api/cypher/CypherAPI.java | 2 +- .../apache/hugegraph/api/CypherApiTest.java | 58 +++++++++++++------ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java index dcf0e2451f..216fff97e9 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java @@ -67,7 +67,7 @@ public class CypherAPI extends API { private static final ObjectReader REQUEST_READER = new ObjectMapper() .readerFor(Object.class) .with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); - private final Base64.Decoder decoder = Base64.getUrlDecoder(); + private final Base64.Decoder decoder = Base64.getDecoder(); private final String basic = "Basic "; private final String bearer = "Bearer "; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java index 2cb5d9af54..0dd9cc3da2 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java @@ -402,14 +402,17 @@ public void testAuthenticationAndGraphRouting() { @Test public void testBasicAuthWithColonPassword() { + // The group, target and access setup below uses the standalone auth API; in PD mode + // groups are graphspace-scoped. The header parsing under test is backend-independent. + assumeStandaloneMode(); // CypherAPI parses the Basic header itself, apart from AuthenticationFilter String user = "cypher_colon_user"; - String password = "a:b:c"; + // With this user name these encode to standard Base64 containing '+' and '/' + Assert.assertTrue(basicCredential(user, "a:b:c~").contains("+")); + Assert.assertTrue(basicCredential(user, "a:b:c?").contains("/")); Deque created = new ArrayDeque<>(); RestClient noAuthClient = new RestClient(baseUrl(), false); try { - String userId = this.createAuth(created, "graphspaces/DEFAULT/auth/users", String.format( - "{\"user_name\":\"%s\",\"user_password\":\"%s\"}", user, password)); String groupId = this.createAuth(created, "auth/groups", "{\"group_name\":\"cypher_colon_group\"}"); String targetId = this.createAuth(created, "graphspaces/DEFAULT/auth/targets", @@ -417,26 +420,35 @@ public void testBasicAuthWithColonPassword() { "\"target_graph\":\"hugegraph\"," + "\"target_url\":\"127.0.0.1:8080\"," + "\"target_resources\":[{\"type\":\"ALL\"}]}"); - this.createAuth(created, "graphspaces/DEFAULT/auth/belongs", String.format( - "{\"user\":\"%s\",\"group\":\"%s\"}", userId, groupId)); for (String permission : ImmutableList.of("READ", "EXECUTE")) { this.createAuth(created, "graphspaces/DEFAULT/auth/accesses", String.format( "{\"group\":\"%s\",\"target\":\"%s\",\"access_permission\":\"%s\"}", groupId, targetId, permission)); } - Response r = basicAuthCypher(noAuthClient, user, password); - assertColumn(assertCypherSuccessData(assertResponseStatus(200, r)), - "name", "marko", "peter"); - - r = basicAuthCypher(noAuthClient, user, "wrong" + password); - assertResponseStatus(401, r); + for (String password : ImmutableList.of("a:b:c", "a:b:c~", "a:b:c?")) { + Deque member = new ArrayDeque<>(); + try { + String userId = this.createAuth(member, "graphspaces/DEFAULT/auth/users", + String.format("{\"user_name\":\"%s\"," + + "\"user_password\":\"%s\"}", + user, password)); + this.createAuth(member, "graphspaces/DEFAULT/auth/belongs", String.format( + "{\"user\":\"%s\",\"group\":\"%s\"}", userId, groupId)); + + Response r = basicAuthCypher(noAuthClient, user, password); + assertColumn(assertCypherSuccessData(assertResponseStatus(200, r)), + "name", "marko", "peter"); + + r = basicAuthCypher(noAuthClient, user, "wrong" + password); + assertResponseStatus(401, r); + } finally { + this.deleteAuth(member); + } + } } finally { noAuthClient.close(); - while (!created.isEmpty()) { - String[] entry = created.pop(); - client().delete(entry[0], entry[1]); - } + this.deleteAuth(created); } } @@ -466,12 +478,22 @@ private String createAuth(Deque created, String path, String body) { return id; } + private void deleteAuth(Deque created) { + while (!created.isEmpty()) { + String[] entry = created.pop(); + client().delete(entry[0], entry[1]); + } + } + + private static String basicCredential(String user, String password) { + byte[] credential = (user + ":" + password).getBytes(StandardCharsets.UTF_8); + return Base64.getEncoder().encodeToString(credential); + } + private static Response basicAuthCypher(RestClient client, String user, String password) { // Built by hand: Jersey's HttpAuthenticationFeature encodes the credential as ISO-8859-1 - byte[] credential = (user + ":" + password).getBytes(StandardCharsets.UTF_8); MultivaluedMap headers = new MultivaluedHashMap<>(); - headers.add(HttpHeaders.AUTHORIZATION, - "Basic " + Base64.getEncoder().encodeToString(credential)); + headers.add(HttpHeaders.AUTHORIZATION, "Basic " + basicCredential(user, password)); return client.target().path(PATH).queryParam("cypher", QUERY) .request().headers(headers).get(); } From cd10ba87c1fa4b30f7b8380061ced37b97b6c879 Mon Sep 17 00:00:00 2001 From: arshil-khan Date: Mon, 12 Oct 2026 02:03:38 +0530 Subject: [PATCH 5/6] docs(helm): point the README guard note at #3284 The paragraph on the admin password guard cited the decoding-site TODO in apache/hugegraph#3260, which this branch removes from AuthenticationFilter. Refer to apache/hugegraph#3284 for the Server side, and keep the reference to the encode_prop_value TODO, which is still in docker-entrypoint.sh. --- helm/hugegraph/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/helm/hugegraph/README.md b/helm/hugegraph/README.md index fb8a01b93d..c8c48bd6c2 100644 --- a/helm/hugegraph/README.md +++ b/helm/hugegraph/README.md @@ -813,8 +813,9 @@ before anything reaches the cluster: applied with the backslashes and answer 401 to the Secret value (measured on `:latest`, 2026-10-03). This guard is temporary: it goes once the Server decodes Basic auth as UTF-8 and splits on the first colon only, and - the entrypoint writes spaces so they read back unchanged (the TODOs at the - decoding site and at `encode_prop_value` are in apache/hugegraph#3260). + the entrypoint writes spaces so they read back unchanged (Server side: + apache/hugegraph#3284, once the image this chart deploys includes that + fix; the TODO at `encode_prop_value` is in apache/hugegraph#3260). - A non-ClusterIP `pd.service.type` requires `pd.service.allowInsecureExposure=true`, a non-ClusterIP `server.service.type` requires `server.service.allowInsecureExposure=true` From c9339f05462b07e712061b313ce118a7057596ac Mon Sep 17 00:00:00 2001 From: arshil-khan Date: Mon, 12 Oct 2026 23:41:27 +0530 Subject: [PATCH 6/6] chore(server): test malformed Basic credentials - LoginApiTest#testBasicAuthWithMalformedCredential sends hand-built headers for "admin", "admin:" and ":pa" and expects 400, then sends "test:test:" and "test::" for the user created in setup() and expects 401, after a control request with "test:test" that returns 200. This covers the 400 paths and the two trailing-colon changes described in the PR. - CypherApiTest#deleteAuth checks each DELETE and, after attempting all of them, fails the test if any auth record was left behind. testBasicAuthWithNonAsciiOrColonPassword asserts 204 on its user deletion for the same reason. --- .../apache/hugegraph/api/CypherApiTest.java | 9 +++++- .../apache/hugegraph/api/LoginApiTest.java | 30 +++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java index 0dd9cc3da2..28d253a5cf 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java @@ -479,10 +479,17 @@ private String createAuth(Deque created, String path, String body) { } private void deleteAuth(Deque created) { + // Attempt every deletion first, then fail on any record that was left behind + List failures = new ArrayList<>(); while (!created.isEmpty()) { String[] entry = created.pop(); - client().delete(entry[0], entry[1]); + Response r = client().delete(entry[0], entry[1]); + if (r.getStatus() != 204) { + failures.add(String.format("%s/%s: %s %s", entry[0], entry[1], + r.getStatus(), r.readEntity(String.class))); + } } + Assert.assertTrue("Failed to delete auth records: " + failures, failures.isEmpty()); } private static String basicCredential(String user, String password) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java index ea6d5eca82..de01f7538e 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/LoginApiTest.java @@ -152,13 +152,13 @@ public void testBasicAuthWithNonAsciiOrColonPassword() { result, new TypeReference>() {}); String id = (String) created.get("id"); try { - r = basicAuthGet(noAuthClient, user[0], user[1]); + r = basicAuthGet(noAuthClient, user[0] + ":" + user[1]); assertResponseStatus(200, r); - r = basicAuthGet(noAuthClient, user[0], "wrong" + user[1]); + r = basicAuthGet(noAuthClient, user[0] + ":wrong" + user[1]); assertResponseStatus(401, r); } finally { - this.deleteUser(id); + assertResponseStatus(204, this.deleteUser(id)); } } } finally { @@ -166,9 +166,29 @@ public void testBasicAuthWithNonAsciiOrColonPassword() { } } - private static Response basicAuthGet(RestClient client, String name, String password) { + @Test + public void testBasicAuthWithMalformedCredential() { + RestClient noAuthClient = new RestClient(baseUrl(), false); + try { + assertResponseStatus(200, basicAuthGet(noAuthClient, "test:test")); + + // No colon, an empty user-id or an empty password + for (String credential : new String[]{"admin", "admin:", ":pa"}) { + assertResponseStatus(400, basicAuthGet(noAuthClient, credential)); + } + + // Only the first colon separates the fields, so a trailing colon belongs to + // the password + assertResponseStatus(401, basicAuthGet(noAuthClient, "test:test:")); + assertResponseStatus(401, basicAuthGet(noAuthClient, "test::")); + } finally { + noAuthClient.close(); + } + } + + private static Response basicAuthGet(RestClient client, String userPass) { // Built by hand: Jersey's HttpAuthenticationFeature encodes the credential as ISO-8859-1 - byte[] credential = (name + ":" + password).getBytes(StandardCharsets.UTF_8); + byte[] credential = userPass.getBytes(StandardCharsets.UTF_8); MultivaluedMap headers = new MultivaluedHashMap<>(); headers.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(credential));