bitflicker64 commented on code in PR #3119:
URL: https://github.com/apache/hugegraph/pull/3119#discussion_r3696241394
##########
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:
The entrypoint auth bootstrap moved out of this PR to #3133. The upgrade
case you describe is listed there as one of the shapes it has to handle — a
legacy volume carrying `init_complete` without the bootstrap state the new code
expects. Left unresolved so it stays findable.
##########
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:
Moved to #3133, listed first there. Agreed the presence-only check cannot
distinguish a partially configured file from a complete one, which is exactly
the Gremlin/REST split you describe.
##########
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:
`ConfigTool` is out of this PR and moves to #3133 with the rest of the
entrypoint work, so this is unaddressed rather than dismissed — write atomicity
needs solving there before it lands again.
##########
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:
Same — `ConfigTool` moves to #3133. The include-semantics point is the
strongest argument there for not round-tripping a mounted config through
`PropertiesConfiguration` at all, and it should shape whatever replaces it.
##########
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:
Moved to #3133 and called out explicitly in it: not passing secrets as
process arguments is one of the stated requirements for that work.
##########
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 `--validate-only` flag is gone from this PR along with the rest of the
entrypoint changes (#3133). What `init-store.sh` keeps here is only the
exit-status propagation, so `ensure_path_writable` runs exactly as it does on
master.
##########
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:
The entrypoint test suite moved to #3133. Agreed the `chmod 444` case does
not model the production failure, since both server Dockerfiles run the
entrypoint as root — worth fixing when the suite comes back.
--
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]