imbajin commented on code in PR #3119:
URL: https://github.com/apache/hugegraph/pull/3119#discussion_r3678544703
##########
hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh:
##########
@@ -54,29 +264,134 @@ migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS"
# ── Map env → properties file ─────────────────────────────────────────
[[ -n "${HG_SERVER_BACKEND:-}" ]] && set_prop "backend"
"${HG_SERVER_BACKEND}" "${GRAPH_CONF}"
[[ -n "${HG_SERVER_PD_PEERS:-}" ]] && set_prop "pd.peers"
"${HG_SERVER_PD_PEERS}" "${GRAPH_CONF}"
+if [[ -n "${HG_SERVER_INIT_STORE_ENABLED:-}" ]]; then
+ # Canonicalize before writing, so the property file only ever holds `true`
+ # or `false` and cannot be read differently by the shell and the server
+ if ! HG_SERVER_INIT_STORE_ENABLED=$(to_bool
"${HG_SERVER_INIT_STORE_ENABLED}"); then
+ log "ERROR: HG_SERVER_INIT_STORE_ENABLED must be a boolean, got
'${HG_SERVER_INIT_STORE_ENABLED}'"
+ exit 1
+ fi
+ set_prop "init_store.enabled" "${HG_SERVER_INIT_STORE_ENABLED}"
"${REST_SERVER_CONF}"
+fi
# ── Build wait-storage env ─────────────────────────────────────────────
WAIT_ENV=()
[[ -n "${HG_SERVER_BACKEND:-}" ]] &&
WAIT_ENV+=("hugegraph.backend=${HG_SERVER_BACKEND}")
[[ -n "${HG_SERVER_PD_PEERS:-}" ]] &&
WAIT_ENV+=("hugegraph.pd.peers=${HG_SERVER_PD_PEERS}")
-# ── Init store (once) ─────────────────────────────────────────────────
-if [[ ! -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ]]; then
+wait_storage() {
if (( ${#WAIT_ENV[@]} > 0 )); then
env "${WAIT_ENV[@]}" ./bin/wait-storage.sh
else
./bin/wait-storage.sh
fi
+}
+
+# ── Init store (once) ─────────────────────────────────────────────────
+# With `init_store.enabled=false` (distributed PD/HStore) init-store is a
no-op:
+# storage owns the metadata and the admin account is created on server startup
+# from `auth.admin_pa`. A requested PASSWORD is therefore written to that
+# property rather than piped into init-store.sh, where it would be read and
+# discarded without creating the account.
+#
+# The value is read back from the config file rather than from the env var, so
+# that a rest-server.properties mounted with the property already set behaves
+# the same as `HG_SERVER_INIT_STORE_ENABLED` (the env mapping above has already
+# been applied, so env still wins).
+INIT_STORE_ENABLED=$(get_prop "init_store.enabled" "${REST_SERVER_CONF}")
+if [[ -n "${INIT_STORE_ENABLED}" ]]; then
+ if ! INIT_STORE_ENABLED=$(to_bool "${INIT_STORE_ENABLED}"); then
+ log "ERROR: init_store.enabled in ${REST_SERVER_CONF} must be a
boolean," \
+ "got '${INIT_STORE_ENABLED}'"
+ exit 1
+ fi
+fi
+
+# A mounted configuration can enable REST authentication without carrying the
+# matching Gremlin handler or auth graph proxy. Complete all three configs for
+# every configured authenticator, whether or not Docker supplied a PASSWORD.
+AUTHENTICATOR=$(get_prop "auth.authenticator" "${REST_SERVER_CONF}")
+if [[ -n "${PASSWORD:-}" || -n "${AUTHENTICATOR}" ]]; then
+ ensure_auth_enabled
+ AUTHENTICATOR=$(get_prop "auth.authenticator" "${REST_SERVER_CONF}")
+fi
+
+AUTH_STATE=""
+AUTH_INIT_REQUIRED=false
+if [[ -n "${AUTHENTICATOR}" ]]; then
+ AUTH_STATE=$(printf '%s\n%s\n%s' \
+ "${AUTHENTICATOR}" \
+ "$(get_prop "auth.remote_url" "${REST_SERVER_CONF}")" \
+ "$(get_prop "auth.graph_store" "${REST_SERVER_CONF}")")
+ STORED_AUTH_STATE=$(cat \
+ "${DOCKER_FOLDER}/${AUTH_INIT_STATE_FILE}" 2>/dev/null || true)
+ if [[ -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" &&
+ "${STORED_AUTH_STATE}" != "${AUTH_STATE}" ]]; then
+ AUTH_INIT_REQUIRED=true
+ fi
+fi
+
+if [[ "${INIT_STORE_ENABLED:-true}" == "false" ]]; then
+ log "init-store disabled; validating the no-op configuration"
+
+ # Let InitStore make the type-aware decision about whether this effective
+ # authenticator needs the built-in admin and whether the configured auth
+ # graph can read the PD-created account. The gate returns before backend or
+ # plugin registration, so this invocation performs validation only.
+ ./bin/init-store.sh
+
+ # Still wait: the server needs the storage side reachable at startup even
+ # though nothing is initialized here
+ wait_storage
+
+ if [[ -n "${PASSWORD:-}" ]]; then
+ log "enabling auth mode, admin password applied via auth.admin_pa"
+ # TODO: auth.admin_pa only applies when the admin account is first
+ # created, so changing PASSWORD on a later restart keeps the old one.
+ if ! ESCAPED_PASSWORD=$(props_escape "${PASSWORD}"); then
+ log "ERROR: PASSWORD must be valid UTF-8"
+ exit 1
+ fi
+ if ! chmod 600 "${REST_SERVER_CONF}"; then
+ log "ERROR: cannot protect ${REST_SERVER_CONF} before writing
auth.admin_pa"
+ exit 1
+ fi
+ if ! set_prop "auth.admin_pa" "${ESCAPED_PASSWORD}" \
+ "${REST_SERVER_CONF}"; then
+ log "ERROR: cannot write auth.admin_pa to ${REST_SERVER_CONF}"
+ exit 1
+ fi
+ fi
+ # No init flag is written here: nothing was initialized, so a later run
+ # with init-store enabled must still perform the real initialization.
+elif [[ ! -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ||
+ "${AUTH_INIT_REQUIRED}" == "true" ]]; then
+ if [[ -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ]]; then
+ log "authentication configuration changed; running init-store once"
+ fi
+ wait_storage
if [[ -z "${PASSWORD:-}" ]]; then
- log "init hugegraph with non-auth mode"
- ./bin/init-store.sh
+ if [[ -n "${AUTHENTICATOR}" ]]; then
+ log "init hugegraph with configured auth.admin_pa"
+ ./bin/init-store.sh --use-configured-admin-password
Review Comment:
‼️ This path treats the option fallback as an explicitly configured secret.
With `auth.authenticator=StandardAuthenticator` but no `PASSWORD` or
`auth.admin_pa`, `InitStore` reads `ServerOptions.ADMIN_PA`, whose public
default is `pa`, and creates the built-in `admin/pa` account; the new smoke
test exercises exactly this missing-password case. Please require an explicitly
present, non-empty admin password before using
`--use-configured-admin-password` (or fail closed), and add a regression
proving the fallback cannot create an administrator.
##########
hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh:
##########
@@ -54,29 +264,134 @@ migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS"
# ── Map env → properties file ─────────────────────────────────────────
[[ -n "${HG_SERVER_BACKEND:-}" ]] && set_prop "backend"
"${HG_SERVER_BACKEND}" "${GRAPH_CONF}"
[[ -n "${HG_SERVER_PD_PEERS:-}" ]] && set_prop "pd.peers"
"${HG_SERVER_PD_PEERS}" "${GRAPH_CONF}"
+if [[ -n "${HG_SERVER_INIT_STORE_ENABLED:-}" ]]; then
+ # Canonicalize before writing, so the property file only ever holds `true`
+ # or `false` and cannot be read differently by the shell and the server
+ if ! HG_SERVER_INIT_STORE_ENABLED=$(to_bool
"${HG_SERVER_INIT_STORE_ENABLED}"); then
+ log "ERROR: HG_SERVER_INIT_STORE_ENABLED must be a boolean, got
'${HG_SERVER_INIT_STORE_ENABLED}'"
+ exit 1
+ fi
+ set_prop "init_store.enabled" "${HG_SERVER_INIT_STORE_ENABLED}"
"${REST_SERVER_CONF}"
+fi
# ── Build wait-storage env ─────────────────────────────────────────────
WAIT_ENV=()
[[ -n "${HG_SERVER_BACKEND:-}" ]] &&
WAIT_ENV+=("hugegraph.backend=${HG_SERVER_BACKEND}")
[[ -n "${HG_SERVER_PD_PEERS:-}" ]] &&
WAIT_ENV+=("hugegraph.pd.peers=${HG_SERVER_PD_PEERS}")
-# ── Init store (once) ─────────────────────────────────────────────────
-if [[ ! -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ]]; then
+wait_storage() {
if (( ${#WAIT_ENV[@]} > 0 )); then
env "${WAIT_ENV[@]}" ./bin/wait-storage.sh
else
./bin/wait-storage.sh
fi
+}
+
+# ── Init store (once) ─────────────────────────────────────────────────
+# With `init_store.enabled=false` (distributed PD/HStore) init-store is a
no-op:
+# storage owns the metadata and the admin account is created on server startup
+# from `auth.admin_pa`. A requested PASSWORD is therefore written to that
+# property rather than piped into init-store.sh, where it would be read and
+# discarded without creating the account.
+#
+# The value is read back from the config file rather than from the env var, so
+# that a rest-server.properties mounted with the property already set behaves
+# the same as `HG_SERVER_INIT_STORE_ENABLED` (the env mapping above has already
+# been applied, so env still wins).
+INIT_STORE_ENABLED=$(get_prop "init_store.enabled" "${REST_SERVER_CONF}")
+if [[ -n "${INIT_STORE_ENABLED}" ]]; then
+ if ! INIT_STORE_ENABLED=$(to_bool "${INIT_STORE_ENABLED}"); then
+ log "ERROR: init_store.enabled in ${REST_SERVER_CONF} must be a
boolean," \
+ "got '${INIT_STORE_ENABLED}'"
+ exit 1
+ fi
+fi
+
+# A mounted configuration can enable REST authentication without carrying the
+# matching Gremlin handler or auth graph proxy. Complete all three configs for
+# every configured authenticator, whether or not Docker supplied a PASSWORD.
+AUTHENTICATOR=$(get_prop "auth.authenticator" "${REST_SERVER_CONF}")
+if [[ -n "${PASSWORD:-}" || -n "${AUTHENTICATOR}" ]]; then
+ ensure_auth_enabled
+ AUTHENTICATOR=$(get_prop "auth.authenticator" "${REST_SERVER_CONF}")
+fi
+
+AUTH_STATE=""
+AUTH_INIT_REQUIRED=false
+if [[ -n "${AUTHENTICATOR}" ]]; then
+ AUTH_STATE=$(printf '%s\n%s\n%s' \
+ "${AUTHENTICATOR}" \
+ "$(get_prop "auth.remote_url" "${REST_SERVER_CONF}")" \
+ "$(get_prop "auth.graph_store" "${REST_SERVER_CONF}")")
+ STORED_AUTH_STATE=$(cat \
+ "${DOCKER_FOLDER}/${AUTH_INIT_STATE_FILE}" 2>/dev/null || true)
+ if [[ -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" &&
+ "${STORED_AUTH_STATE}" != "${AUTH_STATE}" ]]; then
+ AUTH_INIT_REQUIRED=true
+ fi
+fi
+
+if [[ "${INIT_STORE_ENABLED:-true}" == "false" ]]; then
+ log "init-store disabled; validating the no-op configuration"
+
+ # Let InitStore make the type-aware decision about whether this effective
+ # authenticator needs the built-in admin and whether the configured auth
+ # graph can read the PD-created account. The gate returns before backend or
+ # plugin registration, so this invocation performs validation only.
+ ./bin/init-store.sh
+
+ # Still wait: the server needs the storage side reachable at startup even
+ # though nothing is initialized here
+ wait_storage
+
+ if [[ -n "${PASSWORD:-}" ]]; then
+ log "enabling auth mode, admin password applied via auth.admin_pa"
+ # TODO: auth.admin_pa only applies when the admin account is first
+ # created, so changing PASSWORD on a later restart keeps the old one.
+ if ! ESCAPED_PASSWORD=$(props_escape "${PASSWORD}"); then
+ log "ERROR: PASSWORD must be valid UTF-8"
+ exit 1
+ fi
+ if ! chmod 600 "${REST_SERVER_CONF}"; then
+ log "ERROR: cannot protect ${REST_SERVER_CONF} before writing
auth.admin_pa"
+ exit 1
+ fi
+ if ! set_prop "auth.admin_pa" "${ESCAPED_PASSWORD}" \
Review Comment:
⚠️ This persists every supplied `PASSWORD` as `auth.admin_pa`, even when the
effective authenticator is custom or uses `auth.remote_url`; `InitStore`
explicitly exempts those modes because they never consume HugeGraph's built-in
admin password, and the custom-auth test currently asserts the unused secret is
written. Please persist this value only for a local built-in
`StandardAuthenticator` (or subclass), and explicitly ignore or reject it for
custom/remote authentication.
##########
.github/workflows/docker-build-ci.yml:
##########
@@ -26,8 +26,31 @@ on:
paths:
- '**/Dockerfile*'
- '.dockerignore'
+ - 'hugegraph-server/hugegraph-dist/docker/**'
+ -
'hugegraph-server/hugegraph-dist/src/assembly/static/bin/enable-auth.sh'
+ - 'hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh'
+ - '**/docker/docker-entrypoint.sh'
Review Comment:
⚠️ This path filter still lets the entrypoint contract job be skipped when
its Java-side contract changes. The smoke suite depends on `InitStore`,
`StandardAuthenticator`, and the consumed option definitions, but none of those
paths trigger this workflow; its shell stubs also do not exercise their real
status/validation behavior. Please include the relevant Java contract paths or
move this job to an unfiltered/reusable workflow.
##########
hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java:
##########
@@ -210,16 +217,40 @@ public SaslNegotiator newSaslNegotiator(InetAddress
remoteAddress) {
}
public static void initAdminUserIfNeeded(String confFile) throws Exception
{
+ initAdminUserIfNeeded(confFile, null, false);
+ }
+
+ public static void initAdminUserIfNeeded(String confFile,
+ String configuredPassword)
+ throws Exception {
+ initAdminUserIfNeeded(confFile, configuredPassword, true);
+ }
+
+ private static void initAdminUserIfNeeded(String confFile,
+ String password,
+ boolean fromConfig)
+ throws Exception {
StandardAuthenticator auth = new StandardAuthenticator();
HugeConfig config = new HugeConfig(confFile);
String authClass = config.get(ServerOptions.AUTHENTICATOR);
if (authClass.isEmpty()) {
return;
}
config.addProperty(INITING_STORE, true);
- auth.setup(config);
- if (auth.graph().backendStoreFeatures().supportsPersistence()) {
- auth.initAdminUser();
+ auth.initAdminUser(config, password, fromConfig);
+ }
+
+ private void initAdminUser(HugeConfig config, String password,
+ boolean fromConfig) throws Exception {
+ try {
+ this.setup(config);
+ if (this.graph().backendStoreFeatures().supportsPersistence()) {
+ this.initAdminUser(password, fromConfig);
+ }
+ } finally {
+ if (this.graph != null) {
+ this.graph.close();
Review Comment:
⚠️ If `setup()`, feature inspection, or admin creation throws and
`graph.close()` also throws, this `finally` replaces the primary bootstrap
failure with the cleanup exception. The new tests cover each primary failure
only when `close()` succeeds. Please preserve the original exception and attach
a close failure as suppressed (while still surfacing close failure when it is
the only error), and add a dual-failure regression.
##########
hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh:
##########
@@ -19,24 +19,234 @@ set -euo pipefail
DOCKER_FOLDER="./docker"
INIT_FLAG_FILE="init_complete"
+AUTH_INIT_STATE_FILE="auth_init_state"
GRAPH_CONF="./conf/graphs/hugegraph.properties"
+REST_SERVER_CONF="./conf/rest-server.properties"
+GREMLIN_SERVER_CONF="./conf/gremlin-server.yaml"
+
+# The only in-tree HugeAuthenticator that bootstraps HugeGraph's built-in admin
+# account. auth.authenticator accepts any implementation class, and a custom
one
+# (LDAP, OIDC, a plugin) manages its identities elsewhere, so the admin-account
+# requirement below must not be applied to it.
+BUILTIN_AUTHENTICATOR="org.apache.hugegraph.auth.StandardAuthenticator"
mkdir -p "${DOCKER_FOLDER}"
log() { echo "[hugegraph-server-entrypoint] $*"; }
+# Sets a property to exactly one canonical `key=value` line. Existing
+# definitions are matched on any separator a properties file allows (`=`, `:`
+# or whitespace) and collapsed into that single line, because leaving a second
+# definition behind would make the parser expose the key as a list and a scalar
+# read of it would then fail. Comment lines are left alone. Matching is
literal,
+# so no regex escaping of the key or value is needed.
set_prop() {
- local key="$1" val="$2" file="$3"
- local esc_key esc_val
+ local key="$1" val="$2" file="$3" tmp
+
+ # The scratch file holds auth.admin_pa, so keep it off the process umask
+ # and use an unpredictable same-directory name rather than following a
+ # pre-created predictable symlink.
+ if ! tmp=$(umask 077; mktemp "${file}.tmp.XXXXXX"); then
+ return 1
+ fi
+
+ if ! SET_PROP_KEY="$key" SET_PROP_VAL="$val" awk '
+ BEGIN { key = ENVIRON["SET_PROP_KEY"]; val = ENVIRON["SET_PROP_VAL"] }
+ {
+ line = $0
+ probe = line
+ sub(/^[[:space:]]+/, "", probe)
+ if (index(probe, key) == 1) {
+ rest = substr(probe, length(key) + 1)
+ if (rest ~ /^[[:space:]]*[=:]/ || rest ~ /^[[:space:]]+/) {
+ if (!done) { print key "=" val; done = 1 }
+ next
+ }
+ }
+ print line
+ }
+ END { if (!done) print key "=" val }
+ ' "${file}" > "${tmp}"; then
+ rm -f "${tmp}"
+ return 1
+ fi
+
+ # Truncate and rewrite in place rather than rename: a single-file bind
+ # mount cannot be replaced by rename, and a rename would also discard the
+ # original ownership and mode, which matters where auth.admin_pa is written
+ if ! cat "${tmp}" > "${file}"; then
+ rm -f "${tmp}"
+ return 1
+ fi
+ rm -f "${tmp}"
+}
+
+count_prop() {
+ local key="$1" file="$2"
+
+ [[ -f "${file}" ]] || { echo 0; return; }
+ SET_PROP_KEY="$key" awk '
+ BEGIN { key = ENVIRON["SET_PROP_KEY"] }
+ {
+ probe = $0
+ sub(/^[[:space:]]+/, "", probe)
+ if (index(probe, key) == 1) {
+ rest = substr(probe, length(key) + 1)
+ if (rest ~ /^[[:space:]]*[=:]/ || rest ~ /^[[:space:]]+/) {
+ count++
+ }
+ }
+ }
+ END { print count + 0 }
+ ' "${file}"
+}
+
+# Drops duplicate definitions while leaving a single valid definition
untouched.
+# Avoiding a needless rewrite lets complete read-only mounted configs start.
+canonicalize_prop() {
+ local key="$1" file="$2" count cur
+ count=$(count_prop "${key}" "${file}")
+ if (( count > 1 )); then
+ cur=$(get_prop "${key}" "${file}")
+ set_prop "${key}" "${cur}" "${file}"
+ fi
+}
+# Escapes a UTF-8 value for Java-properties serialization. Encoding every
+# UTF-16 code unit as a Unicode escape keeps separators, leading whitespace,
+# backslashes and embedded control characters out of the physical property
+# line while the Java parser reconstructs the exact original string.
+props_escape() {
+ printf '%s' "$1" | iconv -f UTF-8 -t UTF-16BE | \
+ od -An -v -t x1 | awk '
+ {
+ for (i = 1; i <= NF; i++) {
+ if (high == "") {
+ high = $i
+ } else {
+ printf "\\u%s%s", high, $i
+ high = ""
+ }
+ }
+ }
+ END { if (high != "") exit 1 }
+ '
+}
+
+# Echoes the value of a property, or nothing when the key or the file is
+# absent, so callers apply their own default. Accepts the `=`, `:` and
+# whitespace separators that properties files allow. On duplicate keys the last
+# one wins, matching how the properties parser reads the same file. Only
+# surrounding whitespace is trimmed, as the parser does; whitespace inside a
+# value is part of the value and deleting it would corrupt one.
+get_prop() {
+ local key="$1" file="$2"
+ local esc_key
+
+ [[ -f "${file}" ]] || return 0
esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
- esc_val=$(printf '%s' "$val" | sed -e 's/[&|\\]/\\&/g')
+ # '#' delimits the s command because the pattern itself contains '|'.
+ # '-E' rather than '-r': both GNU and BSD sed accept it
+ sed -En
"s#^[[:space:]]*${esc_key}([[:space:]]*[=:]|[[:space:]]+)[[:space:]]*(.*)\$#\\2#p"
\
Review Comment:
‼️ This physical-line reader does not implement the Java-properties grammar
used by `HugeConfig`. For example, Commons Configuration resolves
`auth\.authenticator=...` to `auth.authenticator` and joins a continued
`init_store.enabled=fal\` + next-line `se`, while this reader misses the
escaped key and reads the boolean as `fal\`. The former can enable REST auth
without triggering `ensure_auth_enabled()`, leaving Gremlin authentication and
`HugeFactoryAuthProxy` absent. Please read and rewrite these values through the
same Java/Commons parser, or fully support escaped keys and continuations, with
mounted-config regressions.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]