Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/docker-build-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,22 @@ jobs:
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
java-version: '17'
distribution: 'zulu'
cache: maven
cache-jdk: false
- name: Build the pinned configuration reader
run: |
mvn -B -ntp -pl hugegraph-commons/hugegraph-common -am clean compile dependency:build-classpath \
-Dmdep.outputFile=target/config-readback-classpath.txt -Dmaven.javadoc.skip=true -DskipTests
- name: Verify deployment shell contracts
run: |
bash hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh
HG_CONFIG_TEST_CLASSPATH="$PWD/hugegraph-commons/hugegraph-common/target/classes"
HG_CONFIG_TEST_CLASSPATH+=":$(cat hugegraph-commons/hugegraph-common/target/config-readback-classpath.txt)"
export HG_CONFIG_TEST_CLASSPATH
bash hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh
bash hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-port.sh \
hugegraph-server/hugegraph-dist/src/assembly/static \
Expand Down
21 changes: 21 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@ A non-empty `HUGEGRAPH_ADMIN_PASSWORD` enables Server authentication, and Hubble

`HUGEGRAPH_ADMIN_PASSWORD` initializes the built-in `admin` account on its first authenticated startup. Changing `.env` does not rotate an existing administrator password; use the HugeGraph user API for credential changes.

The Server image writes `PASSWORD` to `auth.admin_pa` in the REST configuration.
It writes `HG_SERVER_AUTH_TOKEN_SECRET` to `auth.token_secret` in both REST and
graph configuration. An explicit signing key must be at least 32 bytes. Authentication
string values retain spaces, tabs, backslashes and Unicode through the Server's
properties reader; keep the same signing key on every replica. Use passwords
compatible with the existing authentication protocol and the chart's documented
password constraints. When the signing-key environment variable is omitted,
the image reuses the configured key instead of rotating it.

Older images could change signing keys containing spaces or Unicode, and a
trailing space could hide the following configuration property. On upgrade,
explicitly provide the intended key for those deployments. Tokens signed with
the previous interpreted key require a new login after the correction.

An older image could also initialize a Unicode administrator password incorrectly
when the initializer inherited the C locale. Upgrading or setting `PASSWORD`
again does not repair an existing account's stored password. A working
administrator must rotate it through the user API. This fix provides no automatic
recovery when no administrator identity is usable; preserve the existing data and
arrange account recovery before upgrading.

For the verification commands below, load `.env` into your current shell and set the password:

```bash
Expand Down
65 changes: 62 additions & 3 deletions hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ EOF
cat > "${TEST_HOME}/bin/init-store.sh" <<'EOF'
#!/usr/bin/env bash
printf 'called\n' >> ./docker/init-store-calls
printf '%s' "${LC_ALL-}" > ./docker/init-store-locale
if IFS= read -r password; then
printf '%s' "${password}" > ./docker/init-store-password
fi
Expand All @@ -67,6 +68,29 @@ exit 0
EOF
chmod +x "${TEST_HOME}/bin/"*.sh

# CI supplies current HugeConfig and the POM-pinned Commons dependency classpath.
if [[ -n "${HG_CONFIG_TEST_CLASSPATH:-}" ]]; then
Comment thread
imbajin marked this conversation as resolved.
javac -cp "${HG_CONFIG_TEST_CLASSPATH}" -d "${TEST_HOME}/docker" \
"${SCRIPT_DIR}/../../hugegraph-core/src/main/java/org/apache/hugegraph/config/AuthOptions.java" \
"${SCRIPT_DIR}/test/AuthConfigReadback.java"
else
echo "SKIP: native auth readback (HG_CONFIG_TEST_CLASSPATH is unset)"
fi
assert_auth_readback() {
if [[ -n "${HG_CONFIG_TEST_CLASSPATH:-}" ]]; then
local secret_base64 admin_base64
secret_base64=$(printf '%s' "$2" | base64 | tr -d '\n')
if [[ $# -eq 3 ]]; then
admin_base64=$(printf '%s' "$3" | base64 | tr -d '\n')
java -Xmx128m -cp "${TEST_HOME}/docker:${HG_CONFIG_TEST_CLASSPATH}" AuthConfigReadback \
"$1" "${secret_base64}" "${admin_base64}"
else
java -Xmx128m -cp "${TEST_HOME}/docker:${HG_CONFIG_TEST_CLASSPATH}" AuthConfigReadback \
"$1" "${secret_base64}"
fi
fi
}

(
cd "${TEST_HOME}"
HG_SERVER_BACKEND=hstore \
Expand Down Expand Up @@ -205,7 +229,7 @@ reused_complex_secret=$(sed -n 's/^auth\.token_secret=//p' \
"${TEST_HOME}/conf/rest-server.properties")
[[ "${reused_complex_secret}" == "${complex_secret}" ]]
[[ "${reused_complex_secret}" == \
'Strong\\Secret\ 9!0123456789abcdef' ]]
'Strong\\Secret\u00209!0123456789abcdef' ]]

(
cd "${TEST_HOME}"
Expand All @@ -222,7 +246,7 @@ trailing_space_secret=$(sed -n 's/^auth\.token_secret=//p' \
reused_trailing_space_secret=$(sed -n 's/^auth\.token_secret=//p' \
"${TEST_HOME}/conf/rest-server.properties")
[[ "${trailing_space_secret}" == \
'SecretEnds\ 0123456789abcdefABCDE\ ' ]]
'SecretEnds\u00200123456789abcdefABCDE\u0020' ]]
[[ "${reused_trailing_space_secret}" == "${trailing_space_secret}" ]]
grep -Fqx 'auth.admin_pa=pa' \
"${TEST_HOME}/conf/rest-server.properties"
Expand All @@ -231,7 +255,7 @@ grep -Fqx 'auth.admin_pa=pa' \
cd "${TEST_HOME}"
PASSWORD='Strong\Pass 9!' bash ./docker-entrypoint.sh
)
grep -Fqx 'auth.admin_pa=Strong\\Pass\ 9!' \
grep -Fqx 'auth.admin_pa=Strong\\Pass\u00209!' \
"${TEST_HOME}/conf/rest-server.properties"

rm -f "${TEST_HOME}/docker/init_complete"
Expand Down Expand Up @@ -329,5 +353,40 @@ done
)
assert_start_timeout 120

# Signing keys are opaque strings: both files must load exactly what the caller supplied.
fake_key='FAKE_SIGNING_KEY_0123456789abcdef'
control_soh=$(printf '\001')
control_us=$(printf '\037')
min_key="${fake_key:0:31}${control_soh}"
[[ ${#min_key} -eq 32 ]]
auth_secrets=("${fake_key}" " ${fake_key}" "${fake_key} inner" "${fake_key} "
"${fake_key}"$'\t' "${fake_key}\\" "${fake_key}café" "${fake_key}中文" "${fake_key}😀"
"${fake_key}\\u0020😀 café " "${fake_key}"$'\377'
"${control_soh}${fake_key}" "${fake_key}${control_soh}" "${fake_key}"$'\v' "${fake_key}${control_us}"
"${min_key}")
fake_admin_password="${control_soh}"' fake\ admin '"${control_us}"
for auth_secret in "${auth_secrets[@]}"; do
cp "${SCRIPT_DIR}/../src/assembly/static/conf/rest-server.properties" \
"${TEST_HOME}/conf/rest-server.properties"
(cd "${TEST_HOME}"; PASSWORD="${fake_admin_password}" HG_SERVER_AUTH_TOKEN_SECRET="${auth_secret}" \
bash ./docker-entrypoint.sh)
expected_secret="${auth_secret}"
if [[ "${auth_secret}" == "${fake_key}"$'\377' ]]; then
expected_secret="${fake_key}ÿ"
fi
assert_auth_readback "${TEST_HOME}/conf/rest-server.properties" "${expected_secret}" "${fake_admin_password}"
assert_auth_readback "${TEST_HOME}/conf/graphs/hugegraph.properties" "${expected_secret}"
cp "${TEST_HOME}/conf/rest-server.properties" "${TEST_HOME}/conf/rest-server.properties.before-reuse"
cp "${TEST_HOME}/conf/graphs/hugegraph.properties" "${TEST_HOME}/conf/graphs/hugegraph.properties.before-reuse"
(cd "${TEST_HOME}"; PASSWORD="${fake_admin_password}" bash ./docker-entrypoint.sh)
cmp "${TEST_HOME}/conf/rest-server.properties.before-reuse" "${TEST_HOME}/conf/rest-server.properties"
cmp "${TEST_HOME}/conf/graphs/hugegraph.properties.before-reuse" "${TEST_HOME}/conf/graphs/hugegraph.properties"
assert_auth_readback "${TEST_HOME}/conf/rest-server.properties" "${expected_secret}" "${fake_admin_password}"
done

(cd "${TEST_HOME}"; LC_ALL=en_US.UTF-8 PASSWORD='locale probe' \
HG_SERVER_AUTH_TOKEN_SECRET="${fake_key}" bash ./docker-entrypoint.sh)
[[ "$(cat "${TEST_HOME}/docker/init-store-locale")" == 'en_US.UTF-8' ]]

echo "PASS: Docker entrypoint configures HStore discovery and authentication"
bash "${SCRIPT_DIR}/test/test-docker-entrypoint-process.sh"
49 changes: 33 additions & 16 deletions hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,13 @@ mkdir -p "${DOCKER_FOLDER}"

log() { echo "[hugegraph-server-entrypoint] $*"; }

# TODO: write values so the Server reads them back unchanged. A space is escaped as "\ " here,
# which java.util.Properties would unescape, but the Server reads these files through Commons
# Configuration's PropertiesConfiguration, which keeps the backslash before a space: PASSWORD
# "a b" becomes auth.admin_pa "a\ b" and the admin login with "a b" answers 401. Emit interior
# spaces as-is (they round-trip) and add a write-then-read test through HugeConfig for the keys
# this script sets. The Helm chart (helm/hugegraph) refuses admin passwords containing spaces in
# its schema and Server wrapper; drop that guard once this is fixed.
# Keep typed/list mappings on their existing encoder; auth strings use native-reader escapes below.
Comment thread
imbajin marked this conversation as resolved.
# TODO: relax Helm's password-space guard once its minimum Server image includes this auth encoder.
encode_prop_value() {
local value="$1" encoded="" char
local i

LC_ALL=C
local LC_ALL=C
for ((i = 0; i < ${#value}; i++)); do
char="${value:i:1}"
case "${char}" in
Expand Down Expand Up @@ -75,6 +70,24 @@ set_prop() {
set_prop_encoded "$key" "$(encode_prop_value "$val")" "$file"
}

# Auth values are strings, not typed/list options. Commons keeps escaped spaces
# and trims a physical trailing space before detecting line continuation.
encode_auth_prop_value() {
local value="$1" encoding=UTF-8
if (( $(printf '%s' "${value}" | LC_ALL=C tr -d '\040-\176' | wc -c) > 0 )); then
# Retain legacy byte values that the ISO-8859-1 reader already accepted.
if ! printf '%s' "${value}" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1; then
encoding=ISO-8859-1
fi
# The Server properties reader is ISO-8859-1. Standard UTF-16 escapes
# preserve UTF-8 input and prevent trimming boundary control characters.
printf '%s' "${value}" | iconv -f "${encoding}" -t UTF-16BE | od -An -v -tx1 | \
tr -d ' \n' | sed 's/..../\\u&/g'
else
encode_prop_value "${value}" | sed 's/\\ /\\u0020/g'
Comment thread
imbajin marked this conversation as resolved.
fi
}

get_prop_encoded() {
local key="$1" file="$2"
local esc_key
Expand All @@ -98,8 +111,7 @@ migrate_env "BACKEND" "HG_SERVER_BACKEND"
migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS"

if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then
LC_ALL=C
if (( ${#HG_SERVER_AUTH_TOKEN_SECRET} < 32 )); then
if (( $(printf '%s' "${HG_SERVER_AUTH_TOKEN_SECRET}" | LC_ALL=C wc -c) < 32 )); then
log "ERROR: HG_SERVER_AUTH_TOKEN_SECRET must be at least 32 bytes"
exit 1
fi
Expand Down Expand Up @@ -152,6 +164,15 @@ if [[ -n "${PASSWORD:-}" && -z "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then
fi
fi

# Encode authentication strings before changing either configuration file.
if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then
AUTH_TOKEN_SECRET_ENCODED=$(encode_auth_prop_value "${HG_SERVER_AUTH_TOKEN_SECRET}")
fi
AUTH_ADMIN_PASSWORD_ENCODED=""
if [[ -n "${PASSWORD:-}" ]]; then
AUTH_ADMIN_PASSWORD_ENCODED=$(encode_auth_prop_value "${PASSWORD}")
fi

# ── Map env → properties file ─────────────────────────────────────────
# TODO: map server.urls_to_pd and server.deploy_in_k8s from the environment here, as PASSWORD is
# mapped to auth.admin_pa below. Without them the Helm chart (helm/hugegraph) rewrites
Expand All @@ -170,18 +191,14 @@ fi
"${HG_SERVER_REST_URL}" "${REST_SERVER_CONF}"
[[ -n "${HG_SERVER_MIN_FREE_MEMORY:-}" ]] && set_prop "restserver.min_free_memory" \
"${HG_SERVER_MIN_FREE_MEMORY}" "${REST_SERVER_CONF}"
if [[ -n "${HG_SERVER_AUTH_TOKEN_SECRET:-}" ]]; then
set_prop "auth.token_secret" "${HG_SERVER_AUTH_TOKEN_SECRET}" \
"${REST_SERVER_CONF}"
set_prop "auth.token_secret" "${HG_SERVER_AUTH_TOKEN_SECRET}" "${GRAPH_CONF}"
elif [[ -n "${AUTH_TOKEN_SECRET_ENCODED}" ]]; then
if [[ -n "${AUTH_TOKEN_SECRET_ENCODED}" ]]; then
set_prop_encoded "auth.token_secret" "${AUTH_TOKEN_SECRET_ENCODED}" \
"${REST_SERVER_CONF}"
set_prop_encoded "auth.token_secret" "${AUTH_TOKEN_SECRET_ENCODED}" \
"${GRAPH_CONF}"
fi
if [[ -n "${PASSWORD:-}" ]]; then
set_prop "auth.admin_pa" "${PASSWORD}" "${REST_SERVER_CONF}"
set_prop_encoded "auth.admin_pa" "${AUTH_ADMIN_PASSWORD_ENCODED}" "${REST_SERVER_CONF}"
# This script is idempotent and must run outside the initialization guard:
# an upgrade can preserve the marker from an unauthenticated deployment.
./bin/enable-auth.sh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.
*/

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;

import org.apache.hugegraph.auth.TokenGenerator;
import org.apache.hugegraph.config.AuthOptions;
import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.config.OptionSpace;

public final class AuthConfigReadback {

public static void main(String[] args) {
OptionSpace.register("auth", AuthOptions.instance());
HugeConfig config = new HugeConfig(args[0]);
String expected = new String(Base64.getDecoder().decode(args[1]), StandardCharsets.UTF_8);
String actual = config.get(AuthOptions.AUTH_TOKEN_SECRET);
if (!expected.equals(actual)) {
throw new AssertionError("Signing key mismatch: expected characters=" + expected.length() +
", loaded characters=" + actual.length());
}
String token = new TokenGenerator(expected).create(Collections.singletonMap("probe", "auth"), 60000L);
if (!"auth".equals(new TokenGenerator(actual).verify(token).get("probe"))) {
throw new AssertionError("Signing key verification mismatch");
}
if (args.length == 3) {
String admin = new String(Base64.getDecoder().decode(args[2]), StandardCharsets.UTF_8);
if (!admin.equals(config.getProperty("auth.admin_pa"))) {
throw new AssertionError("Administrator password mismatch");
}
}
System.out.println("PASS: exact authentication property readback; signing key characters=" + actual.length());
}
}
Loading