Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a7b81ce
fix(hugegraph-dist): skip init-store when graph.load_from_local_confi…
bitflicker64 Jul 26, 2026
69ac25e
fix(hugegraph-dist): gate init-store on a dedicated init_store.enable…
bitflicker64 Jul 26, 2026
e698202
style(hugegraph-dist): avoid em dashes in InitStore comment
bitflicker64 Jul 26, 2026
a62f6ff
fix(hugegraph-dist): refuse auth with init-store skipped unless usePD…
bitflicker64 Jul 27, 2026
3cb4fda
fix(hugegraph-dist): make set_prop match the separators get_prop accepts
bitflicker64 Jul 27, 2026
99d7de8
fix(hugegraph-dist): fail non-zero on unusable skip config, rewrite c…
bitflicker64 Jul 28, 2026
0dd5a7a
fix(hugegraph-dist): keep auth enablement complete and limit the admi…
bitflicker64 Jul 28, 2026
090763a
fix(hugegraph-dist): self-review follow-ups on the init-store skip path
bitflicker64 Jul 28, 2026
4c9be3a
Merge remote-tracking branch 'hugegraph/master' into fix/no-init
bitflicker64 Jul 28, 2026
95bfe45
fix(hugegraph-dist): harden skipped init authentication
bitflicker64 Jul 29, 2026
4b298bf
fix(hugegraph-dist): fail closed on auth config errors
bitflicker64 Jul 29, 2026
906a3ef
fix(hugegraph-api): always close init auth graph
bitflicker64 Jul 29, 2026
1434969
fix(hugegraph-dist): address init-store review findings
bitflicker64 Jul 30, 2026
3e505a8
fix(hugegraph-dist): fail fast on init-store override writes
bitflicker64 Jul 30, 2026
da85e46
fix(server): harden auth bootstrap upgrades
imbajin Jul 31, 2026
ba6b7e7
refactor(hugegraph-dist): narrow init-store gate to what #3118 asks for
bitflicker64 Aug 1, 2026
5268091
style(hugegraph-dist): address review follow-ups on the init-store gate
bitflicker64 Aug 1, 2026
9707feb
fix(hugegraph-dist): fail closed on the default admin password and re…
bitflicker64 Aug 2, 2026
ff1325a
docs(docker): correct the init_complete description after the marker …
bitflicker64 Aug 2, 2026
edf07d0
fix(server): validate the disabled init path on every startup
bitflicker64 Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,32 @@ Configuration is injected via environment variables. The old `docker/configs/app
| `HG_SERVER_BACKEND` | Yes | — | `backend` in `hugegraph.properties` | Storage backend (e.g. `hstore`) |
| `HG_SERVER_PD_PEERS` | Yes | — | `pd.peers` | PD cluster addresses (e.g. `pd0:8686,pd1:8686,pd2:8686`) |
| `STORE_REST` | No | — | Used by `wait-partition.sh` | Store REST endpoint for partition verification (e.g. `store0:8520`) |
| `PASSWORD` | No | — | Enables auth mode | Optional authentication password |
| `PASSWORD` | No | — | Enables auth mode | Optional authentication password; ignored when `HG_SERVER_INIT_STORE_ENABLED` is `false` (see below) |
| `HG_SERVER_INIT_STORE_ENABLED` | No | `true` | `init_store.enabled` in `rest-server.properties` | Set `false` in PD/HStore deployments so init-store skips local backend and admin initialization |

> **The built-in authenticator with `HG_SERVER_INIT_STORE_ENABLED=false`
> requires `usePD=true` and an HStore-backed `auth.graph_store`, unless
> `auth.remote_url` delegates auth elsewhere.** With init-store skipped, the
> server creates the built-in admin in PD metadata, and only an HStore auth
> graph uses the PD-backed auth manager that can read that account. init-store
> exits non-zero when the combination is unusable, rather than leaving a server
> nobody can log in to. A custom `auth.authenticator` is exempt because it
> manages its own identities.
>
> `docker/init_complete` is written by init-store itself, and only after it has
> initialized. A skipped run therefore records nothing, whether it was disabled
> by the variable or by the property in a mounted `rest-server.properties`, so a
> later re-enable is still able to initialize. The marker only short-circuits
> re-initialization: init-store runs on every container start, and a disabled
> one performs the fail-closed check above first, so a marker left by an
> earlier release or an earlier enabled run cannot bypass it.
>
> **`PASSWORD` does not reach that path.** init-store reads it from standard
> input, and a disabled one returns before doing so. The admin is instead
> created from `auth.admin_pa`, whose `pa` default is public, so init-store
> refuses to skip unless it is explicitly set to a non-empty value in a mounted
> `rest-server.properties`. It applies only when the account is first created,
> so changing it later does not rotate an existing password.

**Deprecated aliases** (still work but log a warning):

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,17 @@ public class ServerOptions extends OptionHolder {
"./conf/graphs"
);

public static final ConfigOption<Boolean> INIT_STORE_ENABLED =
new ConfigOption<>(
"init_store.enabled",
"Whether init-store initializes the local backend stores " +
"and the built-in admin account. Set false in distributed " +
"deployments (PD/HStore) where the storage side already " +
"owns the metadata.",
disallowEmpty(),
true
);

public static final ConfigOption<Boolean> SERVER_START_IGNORE_SINGLE_GRAPH_ERROR =
new ConfigOption<>(
"server.start_ignore_single_graph_error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,13 @@ private void loadMetaFromPD() {
this.listenMetaChanges();
}

/**
* Creates the built-in admin account in PD metadata. With init-store
* disabled this is the only bootstrap that admin gets, and init-store's
* fail-closed check assumes it works, so only the already-exists case is
* benign; any other failure aborts startup instead of leaving the server
* without a usable administrator.
*/
public void initAdminUserIfNeeded(String password) {
HugeUser user = new HugeUser("admin");
user.nickname("超级管理员");
Expand All @@ -380,10 +387,29 @@ public void initAdminUserIfNeeded(String password) {
user.create(new Date());
user.avatar("/image.png");
try {
this.metaManager.createUser(user);
try {
this.metaManager.createUser(user);
} catch (Exception e) {
// Judged by re-reading rather than by matching the message:
// benign only if the admin actually exists, from an earlier
// startup or from a concurrent server that won the race
HugeUser existing;
try {
existing = this.metaManager.findUser(user.name());
} catch (Exception probe) {
e.addSuppressed(probe);
throw e;
}
if (existing == null) {
throw e;
}
LOG.info("The built-in admin user already exists, " +
"skip creating it");
}
this.metaManager.initDefaultGraphSpace();
} catch (Exception e) {
LOG.info(e.getMessage());
throw new HugeException("Failed to init the built-in admin " +
"user or the default graph space", e);
}
}

Expand Down
49 changes: 45 additions & 4 deletions hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ set -euo pipefail
DOCKER_FOLDER="./docker"
INIT_FLAG_FILE="init_complete"
GRAPH_CONF="./conf/graphs/hugegraph.properties"
REST_SERVER_CONF="./conf/rest-server.properties"

mkdir -p "${DOCKER_FOLDER}"

Expand Down Expand Up @@ -55,13 +56,39 @@ migrate_env "PD_PEERS" "HG_SERVER_PD_PEERS"
[[ -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}"

# Normalized once here and reused by the init-flag guard below. The accepted
# spellings are the ones HugeConfig accepts, case-insensitive: commons-lang 2.x
# BooleanUtils, reached through commons-configuration 1.x PropertyConverter.
# That set excludes 0 and 1, which commons-lang3 would have taken. Anything
# outside it is rejected now rather than touching the init flag for a value the
# server is going to refuse anyway.
INIT_STORE_ENABLED=$(printf '%s' "${HG_SERVER_INIT_STORE_ENABLED:-}" |
tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
case "${INIT_STORE_ENABLED}" in
"" | y | t | yes | on | true | n | f | no | off | false) ;;
*) log "ERROR: invalid HG_SERVER_INIT_STORE_ENABLED" \
"'${HG_SERVER_INIT_STORE_ENABLED}'"
exit 1 ;;
esac
[[ -n "${INIT_STORE_ENABLED}" ]] && \
set_prop "init_store.enabled" "${INIT_STORE_ENABLED}" "${REST_SERVER_CONF}"

# ── 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
# ── Init store ────────────────────────────────────────────────────────
# init-store owns the marker: it skips re-initialization when the marker is
# present and writes it only after it has actually initialized. Deciding here
# would mean guessing from the environment variable, which says nothing about
# a config mounted with the property already set. Absolute, so the in-Java
# existence check agrees with the guard below no matter where init-store.sh
# leaves its working directory.
INIT_MARKER_PATH="$(cd "${DOCKER_FOLDER}" && pwd)/${INIT_FLAG_FILE}"
export HG_SERVER_INIT_COMPLETE_MARKER="${INIT_MARKER_PATH}"

if [[ ! -f "${INIT_MARKER_PATH}" ]]; then
if (( ${#WAIT_ENV[@]} > 0 )); then
env "${WAIT_ENV[@]}" ./bin/wait-storage.sh
else
Expand All @@ -74,11 +101,25 @@ if [[ ! -f "${DOCKER_FOLDER}/${INIT_FLAG_FILE}" ]]; then
else
log "init hugegraph with auth mode"
./bin/enable-auth.sh
# init-store reads the password from stdin, and a disabled one returns
# before it gets there, so say plainly that PASSWORD is being dropped
case "${INIT_STORE_ENABLED}" in
n | f | no | off | false)
log "WARN: PASSWORD is ignored while init-store is disabled;" \
"the admin is created on the PD startup path from" \
"'auth.admin_pa', which defaults to the public value 'pa'" ;;
esac
echo "${PASSWORD}" | ./bin/init-store.sh
fi
touch "${DOCKER_FOLDER}/${INIT_FLAG_FILE}"
else
log "HugeGraph initialization already done. Skipping re-init..."
log "HugeGraph initialization already done. Revalidating the config..."
# The marker skips re-initialization inside init-store, not init-store
# itself: a disabled one must pass its fail-closed check on every startup,
# because the marker may predate this configuration or this release and
# says nothing about whether the admin the current config relies on is
# reachable. An enabled one returns at the marker, before it touches the
# backend or reads stdin, so neither wait-storage nor PASSWORD is needed.
./bin/init-store.sh
fi

./bin/start-hugegraph.sh -j "${JAVA_OPTS:-}" -t 120
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,9 @@ CP="$CP":$(find -L "${LIB}" -name '*.jar' \! -name 'hugegraph*' | sort | tr '\n'
CP="$CP":$(find -L "${PLUGINS}" -name '*.jar' | sort | tr '\n' ':')
$JAVA -cp $CP ${DEFAULT_JAVA_OPTIONS} \
org.apache.hugegraph.cmd.InitStore "${CONF}"/rest-server.properties
INIT_STORE_STATUS=$?
if [[ ${INIT_STORE_STATUS} -ne 0 ]]; then
exit "${INIT_STORE_STATUS}"
fi

echo "Initialization finished."
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@

package org.apache.hugegraph.cmd;

import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand All @@ -42,6 +47,19 @@ public class InitStore {

private static final Logger LOG = Log.logger(InitStore.class);

/**
* Where to record that initialization actually happened. The caller that
* wants the record supplies the path; nothing is read or written when it
* is unset, so tarball callers are unaffected. A present marker skips
* re-initialization only: the disabled path's fail-closed check runs
* before it is consulted, since a marker left by an earlier release or an
* earlier enabled run says nothing about the current configuration.
*/
public static final String INIT_COMPLETE_MARKER =
"hugegraph.init_complete_marker";
private static final String INIT_COMPLETE_MARKER_ENV =
"HG_SERVER_INIT_COMPLETE_MARKER";

public static void main(String[] args) throws Exception {
E.checkArgument(args.length == 1,
"HugeGraph init-store need to pass the config file " +
Expand All @@ -51,11 +69,39 @@ public static void main(String[] args) throws Exception {

String restConf = args[0];

RegisterUtil.registerBackends();
RegisterUtil.registerPlugins();
// Server options alone can answer the gate below. Backend and plugin
// registration waits for the enabled path, because registerPlugins()
// runs every plugin's register() and propagates its failures.
RegisterUtil.registerServer();

HugeConfig restServerConfig = new HugeConfig(restConf);

/*
* PD/HStore deployments let the storage side own the metadata, so
* init-store has nothing to do; on Kubernetes it re-ran on every Server
* pod restart, since the entrypoint's flag file does not survive one.
* Skipping also skips creating the built-in admin, which only the PD
* startup path can replace, and only for a PD-backed HStore auth graph.
*/
if (!restServerConfig.get(ServerOptions.INIT_STORE_ENABLED)) {
Comment thread
bitflicker64 marked this conversation as resolved.
LOG.warn("Skipping init-store: '{}' is false in '{}'. Local " +
"backend and admin initialization are not performed.",
ServerOptions.INIT_STORE_ENABLED.name(), restConf);
checkAdminBootstrapReachable(restServerConfig, restConf);
Comment thread
bitflicker64 marked this conversation as resolved.
return;
Comment thread
bitflicker64 marked this conversation as resolved.
}

String initedMarker = presentInitCompleteMarker();
if (initedMarker != null) {
LOG.info("Skipping init-store: completion marker '{}' is " +
"present, so this deployment is already initialized",
initedMarker);
return;
Comment thread
bitflicker64 marked this conversation as resolved.
}

RegisterUtil.registerBackends();
RegisterUtil.registerPlugins();

PDAuthConfig.setAuthority(
ServiceConstant.SERVICE_NAME,
ServiceConstant.AUTHORITY);
Expand All @@ -81,6 +127,134 @@ public static void main(String[] args) throws Exception {
}
HugeFactory.shutdown(30L, true);
}

recordInitComplete();
}

private static String configuredInitCompleteMarker() {
String marker = System.getProperty(INIT_COMPLETE_MARKER,
System.getenv(INIT_COMPLETE_MARKER_ENV));
return marker == null || marker.isEmpty() ? null : marker;
}

/**
* The configured marker path, or null when none is configured or the
* file does not exist yet. Consulted only after the disabled-path check,
* so an existing marker can never bypass the fail-closed validation.
*/
private static String presentInitCompleteMarker() {
String marker = configuredInitCompleteMarker();
if (marker != null && Files.exists(Paths.get(marker))) {
return marker;
}
return null;
}

/**
* Only this process knows whether it initialized anything. The Docker
* entrypoint used to decide from its environment variable alone, so a
* mounted config that disabled init-store was still recorded as done and a
* later re-enable skipped for good. Reached only on the enabled path, and
* only after initialization succeeded.
*/
private static void recordInitComplete() throws IOException {
String marker = configuredInitCompleteMarker();
if (marker == null) {
return;
}
Path path = Paths.get(marker);
Path dir = path.toAbsolutePath().getParent();
if (dir != null) {
Files.createDirectories(dir);
}
try {
Files.createFile(path);
} catch (FileAlreadyExistsException e) {
// A concurrent container finishing its own successful init has
// already recorded it, which is the same outcome
}
LOG.info("Recorded init-store completion at '{}'", path);
}

/**
* Skipping leaves the built-in admin to GraphManager.initAdminUserIfNeeded()
* on the PD startup path, which writes it to PD metadata. Only an HStore
* auth graph reads that metadata back, so every other local built-in-auth
* configuration would start a server nobody can log in to. Remote auth and
* custom authenticators keep their identities elsewhere and are exempt.
*/
private static void checkAdminBootstrapReachable(HugeConfig conf,
String restConf) {
if (!requiresLocalBuiltinAdmin(conf)) {
return;
}
if (!conf.get(ServerOptions.USE_PD)) {
Comment thread
bitflicker64 marked this conversation as resolved.
throw unreachableAdmin(restConf, ServerOptions.USE_PD.name() +
" is false");
}

String name = conf.get(ServerOptions.AUTH_GRAPH_STORE);
String path = ConfigUtil.scanGraphsDir(
conf.get(ServerOptions.GRAPHS)).get(name);
if (path == null) {
throw unreachableAdmin(restConf, "auth graph '" + name +
"' has no local configuration");
}
String backend = new HugeConfig(path).get(CoreOptions.BACKEND);
if (!"hstore".equals(backend)) {
throw unreachableAdmin(restConf, "auth graph '" + name +
"' uses backend '" + backend +
"', not 'hstore'");
}

// The server creates the admin from this value and cannot prompt for
// it, and Docker PASSWORD never reaches this path. An absent or empty
// one would hand out the public 'pa' default, so fail instead. Checked
// with containsKey because the default is not a configured secret.
if (!conf.containsKey(ServerOptions.ADMIN_PA.name()) ||
conf.get(ServerOptions.ADMIN_PA).isEmpty()) {
throw unreachableAdmin(restConf, "no explicit non-empty '" +
ServerOptions.ADMIN_PA.name() +
"' is configured, so the admin " +
"would be created with the " +
"public default");
}
}

private static IllegalStateException unreachableAdmin(String restConf,
String reason) {
return new IllegalStateException(String.format(
"Refusing to skip init-store: '%s' configures the built-in " +
"authenticator but %s, so the admin created on the PD startup " +
"path would be unreachable. See docker/README.md.",
restConf, reason));
}

/**
* HugeAuthenticator.loadAuthenticator() accepts any implementation class,
* and only StandardAuthenticator bootstraps HugeGraph's built-in admin
* account. A custom one (LDAP, OIDC, a plugin) manages its identities
* elsewhere, so it must not be held to the requirement above. The class is
* resolved without initializing it, and one that is not on the init-store
* classpath is by definition not the built-in authenticator.
*/
private static boolean requiresLocalBuiltinAdmin(HugeConfig conf) {
if (!conf.get(ServerOptions.AUTH_REMOTE_URL).isEmpty()) {
return false;
}
String authClass = conf.get(ServerOptions.AUTHENTICATOR);
if (authClass.isEmpty()) {
return false;
}
try {
Class<?> clazz = Class.forName(authClass, false,
InitStore.class.getClassLoader());
return StandardAuthenticator.class.isAssignableFrom(clazz);
} catch (ClassNotFoundException | LinkageError e) {
LOG.info("Authenticator '{}' is not on the init-store classpath, " +
"so it is not the built-in one", authClass);
return false;
}
}

private static HugeGraph initGraph(String configPath) throws Exception {
Expand Down
Loading
Loading