imbajin commented on code in PR #3119:
URL: https://github.com/apache/hugegraph/pull/3119#discussion_r3687911099
##########
hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh:
##########
@@ -54,37 +174,165 @@ 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
+ if ! set_prop "init_store.enabled" "${HG_SERVER_INIT_STORE_ENABLED}" \
+ "${REST_SERVER_CONF}"; then
+ log "ERROR: cannot write init_store.enabled to ${REST_SERVER_CONF}"
+ exit 1
+ fi
+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
+
+LOCAL_BUILTIN_AUTH=false
+if [[ -n "${AUTHENTICATOR}" ]] && requires_local_admin; then
+ LOCAL_BUILTIN_AUTH=true
+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}" &&
Review Comment:
‼️ Existing authenticated volumes can fail their first startup after this
upgrade. A legacy volume can have `docker/init_complete` and a working built-in
admin but no new `auth_init_state`; the empty stored state then makes
`AUTH_INIT_REQUIRED=true`, and lines 298-304 reject startup unless the operator
supplies `PASSWORD` or a persisted `auth.admin_pa`. Previous authenticated
initialization passed the password over stdin and did not persist it, so this
turns a valid existing deployment into an unavailable one. Please distinguish
the legacy/missing-state upgrade case and inspect whether admin bootstrap is
actually needed before requiring a new secret; add a regression with
`init_complete`, no `auth_init_state`, an existing admin, and no supplied
password.
##########
hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/ConfigTool.java:
##########
@@ -0,0 +1,211 @@
+/*
+ * 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.cmd;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Collection;
+import java.util.Objects;
+
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.commons.configuration2.PropertiesConfiguration.IOFactory;
+import
org.apache.commons.configuration2.PropertiesConfiguration.PropertiesReader;
+import
org.apache.commons.configuration2.PropertiesConfiguration.PropertiesWriter;
+import org.apache.commons.configuration2.builder.fluent.Configurations;
+import org.apache.commons.configuration2.convert.ListDelimiterHandler;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.io.FileHandler;
+import org.apache.commons.text.StringEscapeUtils;
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.config.ServerOptions;
+import org.apache.hugegraph.dist.RegisterUtil;
+import org.apache.hugegraph.util.E;
+
+public final class ConfigTool {
+
+ private static final String GET = "get";
+ private static final String HAS = "has";
+ private static final String SET = "set";
+ private static final String REQUIRES_LOCAL_ADMIN =
+ "requires-local-admin";
+ private static final String VALIDATE_SKIP = "validate-skip";
+ private static final IOFactory EXACT_PROPERTIES_IO_FACTORY =
+ new IOFactory() {
+
+ @Override
+ public PropertiesReader createPropertiesReader(Reader reader) {
+ return new PropertiesReader(reader);
+ }
+
+ @Override
+ public PropertiesWriter createPropertiesWriter(
+ Writer writer,
+ ListDelimiterHandler delimiterHandler)
{
+ return new PropertiesWriter(writer, delimiterHandler,
+
ConfigTool::escapePropertyValue);
+ }
+ };
+
+ private ConfigTool() {
+ }
+
+ public static void main(String[] args) throws Exception {
+ E.checkArgument(args.length >= 2, "Usage: ConfigTool <command> ...");
+
+ String command = args[0];
+ String file = args[1];
+ switch (command) {
+ case GET:
+ E.checkArgument(args.length == 3,
+ "Usage: ConfigTool get <file> <key>");
+ String value = getProperty(file, args[2]);
+ if (value != null) {
+ // CHECKSTYLE:OFF
+ System.out.print(value);
+ // CHECKSTYLE:ON
+ }
+ break;
+ case HAS:
+ E.checkArgument(args.length == 3,
+ "Usage: ConfigTool has <file> <key>");
+ if (!hasProperty(file, args[2])) {
+ System.exit(1);
+ }
+ break;
+ case SET:
+ E.checkArgument(args.length == 4,
+ "Usage: ConfigTool set <file> <key> <value>");
+ setProperty(file, args[2], args[3]);
+ break;
+ case REQUIRES_LOCAL_ADMIN:
+ E.checkArgument(args.length == 2,
+ "Usage: ConfigTool requires-local-admin " +
+ "<rest-server.properties>");
+ if (!requiresLocalAdmin(file)) {
+ System.exit(1);
+ }
+ break;
+ case VALIDATE_SKIP:
+ E.checkArgument(args.length == 2,
+ "Usage: ConfigTool validate-skip " +
+ "<rest-server.properties>");
+ validateSkip(file);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown command: " +
command);
+ }
+ }
+
+ static String getProperty(String file, String key)
+ throws ConfigurationException {
+ Object value = load(file).getProperty(key);
+ if (value == null) {
+ return null;
+ }
+ E.checkArgument(!(value instanceof Collection),
+ "Property '%s' must contain one value, got '%s'",
+ key, value);
+ return value.toString();
+ }
+
+ static boolean hasProperty(String file, String key)
+ throws ConfigurationException {
+ return load(file).containsKey(key);
+ }
+
+ static void setProperty(String file, String key, String value)
+ throws ConfigurationException, IOException {
+ PropertiesConfiguration config = load(file);
Review Comment:
⚠️ Loading and saving through `PropertiesConfiguration` silently destroys
Commons Configuration include semantics. `include` and `includeoptional` files
are resolved into the effective configuration, but
`PropertiesConfigurationLayout` documents that saving flattens included
properties into the parent and drops the include directive. A single
environment override can therefore stop later child-file updates from applying
and copy included secrets into a broader-permission parent file. Please
preserve include directives during mutation or reject rewriting files that use
them, and add a parent/child include regression.
##########
hugegraph-server/hugegraph-dist/src/main/java/org/apache/hugegraph/cmd/ConfigTool.java:
##########
@@ -0,0 +1,211 @@
+/*
+ * 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.cmd;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Collection;
+import java.util.Objects;
+
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.commons.configuration2.PropertiesConfiguration.IOFactory;
+import
org.apache.commons.configuration2.PropertiesConfiguration.PropertiesReader;
+import
org.apache.commons.configuration2.PropertiesConfiguration.PropertiesWriter;
+import org.apache.commons.configuration2.builder.fluent.Configurations;
+import org.apache.commons.configuration2.convert.ListDelimiterHandler;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.io.FileHandler;
+import org.apache.commons.text.StringEscapeUtils;
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.config.ServerOptions;
+import org.apache.hugegraph.dist.RegisterUtil;
+import org.apache.hugegraph.util.E;
+
+public final class ConfigTool {
+
+ private static final String GET = "get";
+ private static final String HAS = "has";
+ private static final String SET = "set";
+ private static final String REQUIRES_LOCAL_ADMIN =
+ "requires-local-admin";
+ private static final String VALIDATE_SKIP = "validate-skip";
+ private static final IOFactory EXACT_PROPERTIES_IO_FACTORY =
+ new IOFactory() {
+
+ @Override
+ public PropertiesReader createPropertiesReader(Reader reader) {
+ return new PropertiesReader(reader);
+ }
+
+ @Override
+ public PropertiesWriter createPropertiesWriter(
+ Writer writer,
+ ListDelimiterHandler delimiterHandler)
{
+ return new PropertiesWriter(writer, delimiterHandler,
+
ConfigTool::escapePropertyValue);
+ }
+ };
+
+ private ConfigTool() {
+ }
+
+ public static void main(String[] args) throws Exception {
+ E.checkArgument(args.length >= 2, "Usage: ConfigTool <command> ...");
+
+ String command = args[0];
+ String file = args[1];
+ switch (command) {
+ case GET:
+ E.checkArgument(args.length == 3,
+ "Usage: ConfigTool get <file> <key>");
+ String value = getProperty(file, args[2]);
+ if (value != null) {
+ // CHECKSTYLE:OFF
+ System.out.print(value);
+ // CHECKSTYLE:ON
+ }
+ break;
+ case HAS:
+ E.checkArgument(args.length == 3,
+ "Usage: ConfigTool has <file> <key>");
+ if (!hasProperty(file, args[2])) {
+ System.exit(1);
+ }
+ break;
+ case SET:
+ E.checkArgument(args.length == 4,
+ "Usage: ConfigTool set <file> <key> <value>");
+ setProperty(file, args[2], args[3]);
+ break;
+ case REQUIRES_LOCAL_ADMIN:
+ E.checkArgument(args.length == 2,
+ "Usage: ConfigTool requires-local-admin " +
+ "<rest-server.properties>");
+ if (!requiresLocalAdmin(file)) {
+ System.exit(1);
+ }
+ break;
+ case VALIDATE_SKIP:
+ E.checkArgument(args.length == 2,
+ "Usage: ConfigTool validate-skip " +
+ "<rest-server.properties>");
+ validateSkip(file);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown command: " +
command);
+ }
+ }
+
+ static String getProperty(String file, String key)
+ throws ConfigurationException {
+ Object value = load(file).getProperty(key);
+ if (value == null) {
+ return null;
+ }
+ E.checkArgument(!(value instanceof Collection),
+ "Property '%s' must contain one value, got '%s'",
+ key, value);
+ return value.toString();
+ }
+
+ static boolean hasProperty(String file, String key)
+ throws ConfigurationException {
+ return load(file).containsKey(key);
+ }
+
+ static void setProperty(String file, String key, String value)
+ throws ConfigurationException, IOException {
+ PropertiesConfiguration config = load(file);
+ Object current = config.getProperty(key);
+ if (!(current instanceof Collection) &&
+ Objects.equals(current, value)) {
+ return;
+ }
+
+ config.setProperty(key, value);
+ config.setIOFactory(EXACT_PROPERTIES_IO_FACTORY);
+ Path target = new File(file).toPath().toAbsolutePath();
+ Path parent = target.getParent();
+ E.checkState(parent != null, "Config file has no parent: %s", file);
+ Path scratch = Files.createTempFile(parent,
+ target.getFileName() + ".tmp.",
+ null);
+ try {
+ FileHandler handler = new FileHandler(config);
+ handler.save(scratch.toFile());
+ try (InputStream input = Files.newInputStream(scratch);
+ OutputStream output = Files.newOutputStream(
Review Comment:
⚠️ This truncates the active configuration before the replacement bytes are
known to be fully written. An ENOSPC or I/O failure after `TRUNCATE_EXISTING`
leaves the target partial or empty, and the `finally` block then deletes the
complete scratch copy; that can erase the persisted admin credential and make
every restart fail. Please retain a recoverable copy until replacement
succeeds, using atomic replacement for ordinary files and a guarded
backup/restore path for bind mounts, with an injected mid-copy failure test.
##########
hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh:
##########
@@ -54,37 +174,165 @@ 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
+ if ! set_prop "init_store.enabled" "${HG_SERVER_INIT_STORE_ENABLED}" \
+ "${REST_SERVER_CONF}"; then
+ log "ERROR: cannot write init_store.enabled to ${REST_SERVER_CONF}"
+ exit 1
+ fi
+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
+
+LOCAL_BUILTIN_AUTH=false
+if [[ -n "${AUTHENTICATOR}" ]] && requires_local_admin; then
+ LOCAL_BUILTIN_AUTH=true
+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"
+
+ # Validate topology before writing a secret. The final init-store
invocation
+ # below repeats the Java gate after auth.admin_pa has been prepared and
also
+ # enforces that local built-in auth has an explicit non-empty password.
+ validate_skip
+
+ if [[ "${LOCAL_BUILTIN_AUTH}" == "true" ]]; then
+ if [[ -n "${PASSWORD:-}" ]]; then
+ log "enabling built-in auth, 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 ! 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" "${PASSWORD}" \
+ "${REST_SERVER_CONF}"; then
+ log "ERROR: cannot write auth.admin_pa to ${REST_SERVER_CONF}"
+ exit 1
+ fi
+ elif ! require_configured_admin_password; then
+ exit 1
+ fi
+ elif [[ -n "${PASSWORD:-}" ]]; then
+ log "PASSWORD ignored: the configured authenticator does not use
HugeGraph's local built-in admin"
+ fi
+
+ # The gate returns before backend or plugin registration, so this performs
+ # validation only.
+ ./bin/init-store.sh
Review Comment:
⚠️ The documented validation-only disabled path still invokes
`init-store.sh`, whose wrapper unconditionally calls `ensure_path_writable
"${PLUGINS}"` before Java. A hardened deployment with a read-only
application/plugins mount therefore fails even though this path deliberately
skips backend and plugin registration. Please invoke the validation directly
through `ConfigTool`, or add a validation-only wrapper mode that bypasses
plugin writability checks, with a read-only-plugins regression.
##########
hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh:
##########
@@ -19,23 +19,143 @@ 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"
+CONFIG_TOOL="./bin/config-tool.sh"
+
+# 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] $*"; }
+# Property access goes through Commons Configuration, the parser HugeConfig
+# uses. This keeps escaped keys, continuations, duplicate definitions and value
+# serialization identical between the entrypoint and the server.
set_prop() {
- local key="$1" val="$2" file="$3"
- local esc_key esc_val
+ "${CONFIG_TOOL}" set "$3" "$1" "$2"
+}
+
+get_prop() {
+ "${CONFIG_TOOL}" get "$2" "$1"
+}
+
+has_prop() {
+ "${CONFIG_TOOL}" has "$2" "$1"
+}
+
+requires_local_admin() {
+ "${CONFIG_TOOL}" requires-local-admin "${REST_SERVER_CONF}"
+}
- esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
- esc_val=$(printf '%s' "$val" | sed -e 's/[&|\\]/\\&/g')
+validate_skip() {
+ "${CONFIG_TOOL}" validate-skip "${REST_SERVER_CONF}"
+}
+
+# Canonicalizes a boolean the way the server does. HugeConfig parses these
+# options through commons-configuration2 PropertyConverter.toBoolean, i.e.
+# BooleanUtils, which is case-insensitive and accepts y/t/on/yes/true and
+# n/f/no/off/false. The shell must agree with it, or the two layers can
+# disagree about whether to skip: `FALSE` once meant "skip" to Java and "run"
+# to this script. Unrecognized values fail here, as they do in the server.
+to_bool() {
+ case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
+ y|t|on|yes|true) echo "true" ;;
+ n|f|no|off|false) echo "false" ;;
+ *) return 1 ;;
+ esac
+}
+
+gremlin_auth_configured() {
Review Comment:
‼️ This presence-only grep can leave Gremlin and REST using different
authentication providers. For example, after REST moves from
`StandardAuthenticator` to a custom authenticator, an existing Gremlin block is
accepted unchanged, so old HugeGraph credentials can remain valid through
Gremlin. It also misses valid YAML such as `authentication : {...}` and appends
a duplicate block. Please parse and reconcile the effective top-level YAML
authentication configuration with the selected REST authenticator (or fail
closed on mismatch), and add transition plus separator-whitespace regressions.
##########
hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh:
##########
@@ -54,37 +174,165 @@ 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
+ if ! set_prop "init_store.enabled" "${HG_SERVER_INIT_STORE_ENABLED}" \
+ "${REST_SERVER_CONF}"; then
+ log "ERROR: cannot write init_store.enabled to ${REST_SERVER_CONF}"
+ exit 1
+ fi
+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
+
+LOCAL_BUILTIN_AUTH=false
+if [[ -n "${AUTHENTICATOR}" ]] && requires_local_admin; then
+ LOCAL_BUILTIN_AUTH=true
+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"
+
+ # Validate topology before writing a secret. The final init-store
invocation
+ # below repeats the Java gate after auth.admin_pa has been prepared and
also
+ # enforces that local built-in auth has an explicit non-empty password.
+ validate_skip
+
+ if [[ "${LOCAL_BUILTIN_AUTH}" == "true" ]]; then
+ if [[ -n "${PASSWORD:-}" ]]; then
+ log "enabling built-in auth, 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 ! 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" "${PASSWORD}" \
Review Comment:
⚠️ This forwards the administrator password to `ConfigTool set` as a JVM
command-line argument, so it is observable in process listings,
`/proc/<pid>/cmdline`, container inspection, and command-line audit records
during startup. The existing enabled bootstrap path already avoids this by
using stdin. Please add a sensitive-value mode that reads from stdin or a file
descriptor, and test that the launched Java argv never contains the password.
##########
hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh:
##########
@@ -0,0 +1,885 @@
+#!/bin/bash
+#
+# 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.
+#
+# Smoke tests for docker-entrypoint.sh init-store lifecycle.
+#
+# The entrypoint is run against a throwaway install tree whose ./bin scripts
are
+# stubs recording their own invocation, so the tests assert on which scripts
ran
+# and on the resulting config without a backend or Docker. A source-file Java
+# helper verifies password values with the same properties parser semantics.
+#
+# Usage: hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh
+
+set -uo pipefail
+
+SELF_DIR="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ENTRYPOINT="${SELF_DIR}/../docker-entrypoint.sh"
+TEST_CLASSES=$(mktemp -d "${TMPDIR:-/tmp}/hg-entrypoint-classes.XXXXXX")
+javac -d "${TEST_CLASSES}" "${SELF_DIR}/JavaPropertiesReader.java" \
+ "${SELF_DIR}/JavaPropertiesTool.java"
+
+PASS=0
+FAIL=0
+
+fail() {
+ echo " FAIL: $*"
+ FAIL=$((FAIL + 1))
+}
+
+ok() {
+ PASS=$((PASS + 1))
+}
+
+assert_ran() {
+ if grep -qxF "$1" "${INSTALL}/calls.log" 2>/dev/null; then
+ ok
+ else
+ fail "expected '$1' to run; calls were: $(tr '\n' ' ' <
"${INSTALL}/calls.log")"
+ fi
+}
+
+assert_not_ran() {
+ if grep -qxF "$1" "${INSTALL}/calls.log" 2>/dev/null; then
+ fail "expected '$1' NOT to run"
+ else
+ ok
+ fi
+}
+
+assert_file() {
+ if [[ -f "${INSTALL}/$1" ]]; then ok; else fail "expected file '$1' to
exist"; fi
+}
+
+assert_no_file() {
+ if [[ -f "${INSTALL}/$1" ]]; then fail "expected file '$1' NOT to exist";
else ok; fi
+}
+
+assert_output_contains() {
+ if grep -qF "$1" "${INSTALL}/out.log" 2>/dev/null; then
+ ok
+ else
+ fail "expected output to contain '$1': $(cat "${INSTALL}/out.log")"
+ fi
+}
+
+assert_prop() {
+ local expected="$1=$2"
+ if grep -qxF "${expected}" "${INSTALL}/conf/rest-server.properties"
2>/dev/null; then
+ ok
+ else
+ fail "expected property '${expected}' in rest-server.properties"
+ fi
+}
+
+bytes_hex() {
+ printf '%s' "$1" | od -An -v -t x1 | tr -d '[:space:]'
+}
+
+file_mode() {
+ stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"
+}
+
+file_mtime() {
+ stat -c '%Y' "$1" 2>/dev/null || stat -f '%m' "$1"
+}
+
+# Read the generated property through java.util.Properties and compare UTF-8
+# bytes. This catches physical-line truncation and every escape-sequence error,
+# including trailing newlines that command substitution would otherwise hide.
+assert_prop_round_trip() {
+ local key="$1" expected="$2" actual_hex expected_hex
+ if ! actual_hex=$(java -cp "${TEST_CLASSES}" JavaPropertiesReader \
+ "${INSTALL}/conf/rest-server.properties" "${key}" \
+ 2> "${INSTALL}/java-properties.err"); then
+ fail "Java could not read '${key}': $(cat
"${INSTALL}/java-properties.err")"
+ return
+ fi
+ expected_hex=$(bytes_hex "${expected}")
+ if [[ "${actual_hex}" == "${expected_hex}" ]]; then
+ ok
+ else
+ fail "'${key}' parsed as hex '${actual_hex}', expected
'${expected_hex}'"
+ fi
+}
+
+# A scalar option must end up defined exactly once, on any separator, or the
+# properties parser exposes it as a list and a scalar read of it fails
+assert_prop_defined_once() {
+ local n
+ n=$(grep -cE "^[[:space:]]*$1([[:space:]]*[=:]|[[:space:]])" \
+ "${INSTALL}/conf/rest-server.properties" 2>/dev/null || true)
+ if [[ "${n}" == "1" ]]; then ok; else fail "expected '$1' defined once,
found ${n}"; fi
+}
+
+# Matches the separator set assert_prop_defined_once uses, so a `key:value` or
+# `key value` definition cannot pass as absent
+assert_no_prop_key() {
+ if grep -qE "^[[:space:]]*$1([[:space:]]*[=:]|[[:space:]])" \
+ "${INSTALL}/conf/rest-server.properties" 2>/dev/null; then
+ fail "expected no '$1' property"
+ else
+ ok
+ fi
+}
+
+# Auth is only fully enabled when all three configs agree: the REST properties,
+# the gremlin-server.yaml authentication block and the graph's auth proxy
+assert_auth_fully_enabled() {
+ local n
+ n=$(grep -cE '^[[:space:]]*authentication:' \
+ "${INSTALL}/conf/gremlin-server.yaml" 2>/dev/null || true)
+ if [[ "${n}" == "1" ]]; then
+ ok
+ else
+ fail "expected one gremlin-server.yaml authentication block, found
${n}"
+ fi
+ if grep -qxF
"gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy" \
+ "${INSTALL}/conf/graphs/hugegraph.properties" 2>/dev/null; then
+ ok
+ else
+ fail "expected hugegraph.properties to use HugeFactoryAuthProxy"
+ fi
+ assert_prop_defined_once "auth.authenticator"
+ assert_prop_defined_once "auth.graph_store"
+}
+
+# Build a throwaway install tree with stubbed bin scripts
+new_install() {
+ INSTALL=$(mktemp -d "${TMPDIR:-/tmp}/hg-entrypoint-test.XXXXXX")
+ mkdir -p "${INSTALL}/bin" "${INSTALL}/conf/graphs"
+
+ # Mirrors the shipped conf: the auth properties are present but commented
+ cat > "${INSTALL}/conf/rest-server.properties" <<'EOF'
+restserver.url=http://0.0.0.0:8080
+graphs=./conf/graphs
+#auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator
+#auth.admin_pa=pa
+EOF
+ chmod 644 "${INSTALL}/conf/rest-server.properties"
+ cat > "${INSTALL}/conf/graphs/hugegraph.properties" <<'EOF'
+backend=rocksdb
+gremlin.graph=org.apache.hugegraph.HugeFactory
+EOF
+ # Shipped without an authentication block, which is what enable-auth.sh
adds
+ echo "host: 0.0.0.0" > "${INSTALL}/conf/gremlin-server.yaml"
+
+ local script
+ for script in wait-storage start-hugegraph wait-partition; do
+ cat > "${INSTALL}/bin/${script}.sh" <<EOF
+#!/bin/bash
+echo "${script}.sh" >> "${INSTALL}/calls.log"
+EOF
+ chmod +x "${INSTALL}/bin/${script}.sh"
+ done
+
+ # Records whether a password was piped in, which is how the entrypoint
+ # passes a Docker PASSWORD to the admin bootstrap
+ cat > "${INSTALL}/bin/init-store.sh" <<EOF
+#!/bin/bash
+echo "init-store.sh" >> "${INSTALL}/calls.log"
+if [[ \$# -gt 0 ]]; then
+ echo "init-store.sh:args=\$*" >> "${INSTALL}/calls.log"
+fi
+if [[ ! -t 0 ]]; then
+ stdin=\$(cat)
+ [[ -n "\${stdin}" ]] && echo "init-store.sh:stdin=\${stdin}" >>
"${INSTALL}/calls.log"
+fi
+exit "\${INIT_STORE_STUB_RC:-0}"
+EOF
+ chmod +x "${INSTALL}/bin/init-store.sh"
+
+ # The production wrapper launches ConfigTool from the assembled jars. This
+ # source-launch stub keeps the shell suite backend-free while using Java's
+ # properties grammar for escaped keys and continued logical lines.
+ cat > "${INSTALL}/bin/config-tool.sh" <<EOF
+#!/bin/bash
+if [[ "\$1" == "validate-skip" ]]; then
+ exit "\${INIT_STORE_STUB_RC:-0}"
+fi
+if [[ "\$1" == "requires-local-admin" ]]; then
+ authenticator=\$(java -cp "${TEST_CLASSES}" JavaPropertiesTool get "\$2" \
+ auth.authenticator)
+ remote_url=\$(java -cp "${TEST_CLASSES}" JavaPropertiesTool get "\$2" \
+ auth.remote_url)
+ [[ "\${authenticator}" == \
+ "org.apache.hugegraph.auth.StandardAuthenticator" && \
+ -z "\${remote_url}" ]]
+ exit
+fi
+exec java -cp "${TEST_CLASSES}" JavaPropertiesTool "\$@"
+EOF
+ chmod +x "${INSTALL}/bin/config-tool.sh"
+
+ # Mirrors bin/enable-auth.sh: appends the REST keys and the YAML
+ # authentication block and switches the graph to the auth proxy, but only
+ # before its one-time conf-bak guard exists.
+ cat > "${INSTALL}/bin/enable-auth.sh" <<EOF
+#!/bin/bash
+echo "enable-auth.sh" >> "${INSTALL}/calls.log"
+if [[ ! -d "${INSTALL}/conf-bak" ]]; then
+mkdir -p "${INSTALL}/conf-bak"
+{
+ echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator"
+ echo "auth.graph_store=hugegraph"
+} >> "${INSTALL}/conf/rest-server.properties"
+cat >> "${INSTALL}/conf/gremlin-server.yaml" <<'YAML'
+authentication: {
+ authenticator: org.apache.hugegraph.auth.StandardAuthenticator,
+ authenticationHandler: org.apache.hugegraph.auth.WsAndHttpBasicAuthHandler,
+ config: {tokens: conf/rest-server.properties}
+}
+YAML
+sed -i.bak
's/gremlin.graph=org.apache.hugegraph.HugeFactory/gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy/g'
\
+ "${INSTALL}/conf/graphs/hugegraph.properties"
+rm -f "${INSTALL}/conf/graphs/hugegraph.properties.bak"
+fi
+EOF
+ chmod +x "${INSTALL}/bin/enable-auth.sh"
+
+ : > "${INSTALL}/calls.log"
+}
+
+# The built-in admin created on the PD path is usable only when the auth graph
+# selects HStore's PD-backed auth manager.
+enable_pd() {
+ echo "usePD=true" >> "${INSTALL}/conf/rest-server.properties"
+ sed -i.bak 's/^backend=rocksdb$/backend=hstore/' \
+ "${INSTALL}/conf/graphs/hugegraph.properties"
+ rm -f "${INSTALL}/conf/graphs/hugegraph.properties.bak"
+}
+
+# Run the entrypoint inside the throwaway tree. No ./bin/pid is ever written by
+# the stubs, so the entrypoint's tail-on-pid block is skipped and it returns.
+run_entrypoint() {
+ ( cd "${INSTALL}" && env "$@" bash "${ENTRYPOINT}" ) >
"${INSTALL}/out.log" 2>&1
+ local rc=$?
+ if [[ ${rc} -ne 0 ]]; then
+ fail "entrypoint exited ${rc}; output: $(cat "${INSTALL}/out.log")"
+ fi
+ return 0
+}
+
+run_entrypoint_fails() {
+ if ( cd "${INSTALL}" && env "$@" bash "${ENTRYPOINT}" ) \
+ > "${INSTALL}/out.log" 2>&1; then
+ fail "expected entrypoint to fail"
+ else
+ ok
+ fi
+}
+
+cleanup() { [[ -n "${INSTALL:-}" ]] && rm -rf "${INSTALL}"; }
+cleanup_all() {
+ cleanup
+ rm -rf "${TEST_CLASSES}"
+}
+trap cleanup_all EXIT
+
+echo "==> default: no flag set, full init runs"
+new_install
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED -u PASSWORD
+assert_ran "wait-storage.sh"
+assert_ran "init-store.sh"
+assert_not_ran "enable-auth.sh"
+assert_file "docker/init_complete"
+cleanup
+
+echo "==> default + PASSWORD: auth enabled, password piped to init-store"
+new_install
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED PASSWORD=s3cret
+assert_ran "enable-auth.sh"
+assert_ran "init-store.sh"
+assert_ran "init-store.sh:stdin=s3cret"
+assert_file "docker/init_complete"
+assert_auth_fully_enabled
+cleanup
+
+echo "==> skip via env: InitStore validates the no-op and no flag is written"
+new_install
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=false
+assert_ran "wait-storage.sh"
+assert_ran "init-store.sh"
+assert_prop "init_store.enabled" "false"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> skip + PASSWORD: password reaches auth.admin_pa, not init-store
stdin"
+new_install
+enable_pd
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false PASSWORD=s3cret
+assert_ran "enable-auth.sh"
+assert_ran "init-store.sh"
+assert_not_ran "init-store.sh:stdin=s3cret"
+assert_prop_round_trip "auth.admin_pa" "s3cret"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> skip via mounted property only: env var absent behaves the same"
+new_install
+enable_pd
+echo "init_store.enabled=false" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED PASSWORD=s3cret
+assert_ran "init-store.sh"
+assert_not_ran "init-store.sh:stdin=s3cret"
+assert_prop_round_trip "auth.admin_pa" "s3cret"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> env wins over a conflicting mounted property"
+new_install
+echo "init_store.enabled=false" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=true
+assert_ran "init-store.sh"
+assert_file "docker/init_complete"
+cleanup
+
+echo "==> false then true: a restart with init enabled still initializes"
+new_install
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=false
+assert_ran "init-store.sh"
+assert_no_file "docker/init_complete"
+: > "${INSTALL}/calls.log"
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=true
+assert_ran "init-store.sh"
+assert_file "docker/init_complete"
+cleanup
+
+echo "==> restart with init enabled: the flag file suppresses re-init"
+new_install
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED -u PASSWORD
+assert_ran "init-store.sh"
+: > "${INSTALL}/calls.log"
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED -u PASSWORD
+assert_not_ran "init-store.sh"
+assert_file "docker/init_complete"
+cleanup
+
+echo "==> adding PASSWORD after no-auth init still creates the admin"
+new_install
+mkdir -p "${INSTALL}/docker"
+touch "${INSTALL}/docker/init_complete"
+run_entrypoint PASSWORD=s3cret
+assert_ran "enable-auth.sh"
+assert_ran "init-store.sh"
+assert_ran "init-store.sh:stdin=s3cret"
+assert_auth_fully_enabled
+assert_file "docker/init_complete"
+assert_file "docker/auth_init_state"
+cleanup
+
+echo "==> mounted built-in auth after no-auth init still creates the admin"
+new_install
+mkdir -p "${INSTALL}/docker"
+touch "${INSTALL}/docker/init_complete"
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint PASSWORD=s3cret
+assert_ran "init-store.sh"
+assert_ran "init-store.sh:stdin=s3cret"
+assert_auth_fully_enabled
+assert_file "docker/auth_init_state"
+cleanup
+
+echo "==> mounted built-in auth without PASSWORD uses an explicit admin
password"
+new_install
+mkdir -p "${INSTALL}/docker"
+touch "${INSTALL}/docker/init_complete"
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+echo "auth.admin_pa=s3cret" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u PASSWORD
+assert_ran "init-store.sh"
+assert_ran "init-store.sh:args=--use-configured-admin-password"
+assert_auth_fully_enabled
+assert_file "docker/auth_init_state"
+: > "${INSTALL}/calls.log"
+run_entrypoint -u PASSWORD
+assert_not_ran "init-store.sh"
+assert_ran "start-hugegraph.sh"
+cleanup
+
+echo "==> an existing conf-bak cannot suppress requested auth"
+new_install
+mkdir -p "${INSTALL}/conf-bak"
+run_entrypoint PASSWORD=s3cret
+assert_ran "enable-auth.sh"
+assert_ran "init-store.sh:stdin=s3cret"
+assert_auth_fully_enabled
+cleanup
+
+echo "==> uppercase FALSE is honoured, matching the server's boolean parsing"
+new_install
+enable_pd
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=FALSE PASSWORD=s3cret
+assert_ran "init-store.sh"
+assert_not_ran "init-store.sh:stdin=s3cret"
+assert_prop "init_store.enabled" "false"
+assert_prop_round_trip "auth.admin_pa" "s3cret"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> 'off' and 'no' are honoured too"
+for value in off no; do
+ new_install
+ run_entrypoint -u PASSWORD "HG_SERVER_INIT_STORE_ENABLED=${value}"
+ assert_ran "init-store.sh"
+ assert_no_file "docker/init_complete"
+ cleanup
+done
+
+echo "==> a non-boolean value fails fast instead of diverging"
+new_install
+if ( cd "${INSTALL}" && env -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=maybe \
+ bash "${ENTRYPOINT}" ) >/dev/null 2>&1; then
+ fail "expected a non-boolean HG_SERVER_INIT_STORE_ENABLED to fail"
+else
+ ok
+fi
+assert_not_ran "start-hugegraph.sh"
+cleanup
+
+echo "==> mounted property with a ':' separator is honoured"
+new_install
+echo "init_store.enabled:false" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED -u PASSWORD
+assert_ran "init-store.sh"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> a continued mounted boolean is parsed as one Java property"
+new_install
+{
+ printf 'init_store.enabled=fal\\\n'
+ echo ' se'
+} >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED -u PASSWORD
+assert_ran "init-store.sh"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> password with backslashes survives the properties round trip"
+new_install
+enable_pd
+run_entrypoint 'PASSWORD=abc\def' HG_SERVER_INIT_STORE_ENABLED=false
+assert_ran "init-store.sh"
+assert_prop_round_trip "auth.admin_pa" 'abc\def'
+cleanup
+
+echo "==> properties metacharacters, controls and UTF-8 round-trip exactly"
+new_install
+enable_pd
+complex_password=$' \tmeta:=#!\\\r\f\np\xc3\xa4ss\xe9\x9b\xaa'
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false \
+ "PASSWORD=${complex_password}"
+assert_prop_round_trip "auth.admin_pa" "${complex_password}"
+assert_prop_defined_once "auth.admin_pa"
+cleanup
+
+echo "==> a trailing newline survives the properties round trip"
+new_install
+enable_pd
+trailing_newline_password=$'ends-with-newline\n'
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false \
+ "PASSWORD=${trailing_newline_password}"
+assert_prop_round_trip "auth.admin_pa" "${trailing_newline_password}"
+cleanup
+
+echo "==> a Java validation failure is propagated before password persistence"
+new_install
+run_entrypoint_fails -u HG_SERVER_INIT_STORE_ENABLED PASSWORD=s3cret \
+ HG_SERVER_INIT_STORE_ENABLED=false INIT_STORE_STUB_RC=1
+assert_not_ran "start-hugegraph.sh"
+assert_not_ran "init-store.sh"
+assert_no_prop_key "auth.admin_pa"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> skip with auth already in a mounted config, no usePD, is refused too"
+new_install
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint_fails -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=false \
+ INIT_STORE_STUB_RC=1
+assert_not_ran "start-hugegraph.sh"
+assert_not_ran "init-store.sh"
+cleanup
+
+echo "==> skip without auth is unaffected by the usePD requirement"
+new_install
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=false
+assert_ran "start-hugegraph.sh"
+assert_ran "init-store.sh"
+assert_no_file "docker/init_complete"
+cleanup
+
+echo "==> env override of a colon-form property leaves one canonical key"
+new_install
+echo "init_store.enabled:false" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=true
+assert_prop_defined_once "init_store.enabled"
+assert_prop "init_store.enabled" "true"
+assert_ran "init-store.sh"
+cleanup
+
+echo "==> env override of a whitespace-form property leaves one canonical key"
+new_install
+echo "init_store.enabled false" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=true
+assert_prop_defined_once "init_store.enabled"
+assert_prop "init_store.enabled" "true"
+assert_ran "init-store.sh"
+cleanup
+
+echo "==> PASSWORD override of a colon-form auth.admin_pa leaves one key"
+new_install
+enable_pd
+echo "auth.admin_pa:old" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false PASSWORD=s3cret
+assert_prop_defined_once "auth.admin_pa"
+assert_prop_round_trip "auth.admin_pa" "s3cret"
+cleanup
+
+echo "==> commented-out defaults are not treated as definitions"
+new_install
+run_entrypoint -u HG_SERVER_INIT_STORE_ENABLED PASSWORD=s3cret
+# The shipped file ships '#auth.admin_pa=pa' commented; it must stay commented
+# and must not count as an existing definition
+if grep -qxF "#auth.admin_pa=pa" "${INSTALL}/conf/rest-server.properties"; then
+ ok
+else
+ fail "expected the commented '#auth.admin_pa=pa' line to be preserved"
+fi
+cleanup
+
+echo "==> mounted config that already enables auth is not duplicated"
+new_install
+enable_pd
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+echo "auth.graph_store=hugegraph" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false PASSWORD=s3cret
+assert_not_ran "enable-auth.sh"
+assert_prop_defined_once "auth.admin_pa"
+assert_prop_round_trip "auth.admin_pa" "s3cret"
+# The mounted config carried only the REST keys, so the YAML block and the auth
+# proxy still have to be applied, or Gremlin would stay unauthenticated
+assert_auth_fully_enabled
+cleanup
+
+echo "==> mounted config with only auth.authenticator still authenticates
Gremlin"
+new_install
+enable_pd
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false PASSWORD=s3cret
+assert_not_ran "enable-auth.sh"
+assert_auth_fully_enabled
+assert_prop "auth.graph_store" "hugegraph"
+cleanup
+
+echo "==> mounted auth without PASSWORD still protects Gremlin and the graph"
+new_install
+enable_pd
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+echo "auth.admin_pa=s3cret" >> "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u PASSWORD HG_SERVER_INIT_STORE_ENABLED=false
+assert_ran "init-store.sh"
+assert_auth_fully_enabled
+assert_prop_round_trip "auth.admin_pa" "s3cret"
+cleanup
+
+echo "==> a gremlin-server.yaml without a trailing newline is still valid"
+new_install
+enable_pd
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+printf 'host: 0.0.0.0' > "${INSTALL}/conf/gremlin-server.yaml"
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false PASSWORD=s3cret
+assert_auth_fully_enabled
+# The appended block must start on its own line, not glued onto 'host: 0.0.0.0'
+if grep -qxF "host: 0.0.0.0" "${INSTALL}/conf/gremlin-server.yaml"; then
+ ok
+else
+ fail "the last pre-existing line was absorbed by the appended block"
+fi
+cleanup
+
+echo "==> a pre-authenticated mount is completed, not duplicated"
+new_install
+enable_pd
+# Everything already in place, as after a restart with the conf dir mounted
+"${INSTALL}/bin/enable-auth.sh"
+: > "${INSTALL}/calls.log"
+run_entrypoint HG_SERVER_INIT_STORE_ENABLED=false PASSWORD=s3cret
+assert_not_ran "enable-auth.sh"
+assert_auth_fully_enabled
+cleanup
+
+echo "==> a complete read-only auth mount is not rewritten"
+new_install
+"${INSTALL}/bin/enable-auth.sh"
+echo "auth.admin_pa=s3cret" >> "${INSTALL}/conf/rest-server.properties"
+: > "${INSTALL}/calls.log"
+touch -t 202001010000 "${INSTALL}/conf/rest-server.properties"
+before_mtime=$(file_mtime "${INSTALL}/conf/rest-server.properties")
+chmod 444 "${INSTALL}/conf/rest-server.properties"
+run_entrypoint -u PASSWORD
+after_mtime=$(file_mtime "${INSTALL}/conf/rest-server.properties")
+after_mode=$(file_mode "${INSTALL}/conf/rest-server.properties")
+assert_ran "init-store.sh:args=--use-configured-admin-password"
+assert_ran "start-hugegraph.sh"
+assert_auth_fully_enabled
+if [[ "${after_mtime}" == "${before_mtime}" ]]; then
+ ok
+else
+ fail "read-only rest-server.properties was rewritten"
+fi
+if [[ "${after_mode}" == "444" ]]; then
+ ok
+else
+ fail "read-only rest-server.properties mode became ${after_mode}"
+fi
+cleanup
+
+echo "==> an incomplete read-only auth mount fails with the missing property"
+new_install
+echo "auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator" \
+ >> "${INSTALL}/conf/rest-server.properties"
+chmod 444 "${INSTALL}/conf/rest-server.properties"
+run_entrypoint_fails -u PASSWORD
+assert_not_ran "init-store.sh"
+assert_not_ran "start-hugegraph.sh"
+assert_output_contains \
+ "ERROR: cannot write auth.graph_store to ./conf/rest-server.properties"
+cleanup
+
+echo "==> a read-only mount rejects an init-store env override"
+new_install
+chmod 444 "${INSTALL}/conf/rest-server.properties"
Review Comment:
⚠️ `chmod 444` does not model the production container failure this test
claims to cover. Both server Dockerfiles run the entrypoint as root, which can
still truncate a regular mode-0444 file, while Actions runs this shell suite as
a non-root host user; the test therefore never exercises a real read-only bind
mount/EROFS path. Please add an image-level case with a genuinely read-only
bind mount or filesystem and assert non-zero propagation, no server start, and
no leaked scratch file.
--
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]