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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hugegraph-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ RUN apt-get -q update \
COPY hugegraph-server/hugegraph-dist/docker/scripts/remote-connect.groovy ./scripts
COPY hugegraph-server/hugegraph-dist/docker/scripts/detect-storage.groovy ./scripts
COPY hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh .
COPY hugegraph-server/hugegraph-dist/docker/props.awk .
RUN chmod 755 ./docker-entrypoint.sh

EXPOSE 8080
Expand Down
1 change: 1 addition & 0 deletions hugegraph-server/Dockerfile-hstore
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ RUN apt-get -q update \
COPY hugegraph-server/hugegraph-dist/docker/scripts/remote-connect.groovy ./scripts
#COPY hugegraph-server/hugegraph-dist/docker/scripts/detect-storage.groovy ./scripts
COPY hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh .
COPY hugegraph-server/hugegraph-dist/docker/props.awk .
RUN chmod 755 ./docker-entrypoint.sh

EXPOSE 8080
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ trap 'rm -rf "${TEST_HOME}"' EXIT

mkdir -p "${TEST_HOME}/bin" "${TEST_HOME}/conf/graphs" "${TEST_HOME}/docker"
cp "${SCRIPT_DIR}/docker-entrypoint.sh" "${TEST_HOME}/docker-entrypoint.sh"
cp "${SCRIPT_DIR}/props.awk" "${TEST_HOME}/props.awk"
touch "${TEST_HOME}/docker/init_complete"

cat > "${TEST_HOME}/conf/rest-server.properties" <<'EOF'
Expand Down
168 changes: 152 additions & 16 deletions hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ mkdir -p "${DOCKER_FOLDER}"

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

# Property reading/writing goes through props.awk, which implements the
# java.util.Properties grammar HugeConfig applies (escapes, `:`/whitespace
# separators, continuations, first-definition-wins duplicates). grep/sed
# rewrites disagree with it on mounted or upgraded configs, silently
# producing two definitions of one key. Values move through environment
# variables rather than argv so a PASSWORD never shows up in `ps` output.
PROPS_AWK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/props.awk"
if [[ ! -f "${PROPS_AWK}" ]]; then
log "ERROR: props.awk not found next to the entrypoint"
exit 1
fi

encode_prop_value() {
local value="$1" encoded="" char
local i
Expand All @@ -48,18 +60,10 @@ encode_prop_value() {

set_prop_encoded() {
local key="$1" encoded_val="$2" file="$3"
local esc_key esc_val key_re

esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
esc_val=$(printf '%s' "$encoded_val" | sed -e 's/[&|\\~]/\\&/g')
key_re="^[[:space:]]*${esc_key}([[:space:]]*[:=]|[[:space:]]+|[[:space:]]*$)"

if grep -qE "${key_re}" "${file}"; then
sed -ri "0,/${key_re}/!{/${key_re}/d;}" "${file}"
sed -ri "0,/${key_re}/s~${key_re}.*~${key}=${esc_val}~" "${file}"
else
printf '%s=%s\n' "$key" "$encoded_val" >> "${file}"
fi
PROPS_MODE=set PROPS_KEY="${key}" \
PROPS_VALUE_ENCODED="${encoded_val}" PROPS_FILE="${file}" \
awk -f "${PROPS_AWK}" /dev/null
}

set_prop() {
Expand All @@ -70,12 +74,141 @@ set_prop() {

get_prop_encoded() {
local key="$1" file="$2"
local esc_key

esc_key=$(printf '%s' "$key" | sed -e 's/[][(){}.^$*+?|\\/]/\\&/g')
sed -nE \
"s~^[[:space:]]*${esc_key}([[:space:]]*[:=][[:space:]]*|[[:space:]]+)(.*)$~\\2~p" \
"${file}" | head -n 1
PROPS_MODE=get PROPS_KEY="${key}" PROPS_FILE="${file}" \
awk -f "${PROPS_AWK}" /dev/null
}

# Decoded read: unescapes the on-disk value the way java.util.Properties
# does, so it compares equal with the snakeyaml-decoded scalar from
# get_yaml_authenticator. The raw get_prop_encoded mode stays for the
# secret round trip, which must replay backslashes byte-for-byte.
get_prop() {
local key="$1" file="$2"

PROPS_MODE=get-decoded PROPS_KEY="${key}" PROPS_FILE="${file}" \
awk -f "${PROPS_AWK}" /dev/null
}

# First uncommented `authenticator:` inside the gremlin-server.yaml
# authentication block, or on the `authentication:` line itself (a flow
# mapping). snakeyaml resolves duplicate top-level keys to the last one,
# but a mounted file carrying two authentication blocks is pathological;
# report the first and let the mismatch WARN handle it. The scalar is
# cleaned the way snakeyaml reads it — an inline comment (a '#' preceded
# by whitespace), surrounding quotes and padding are stripped — because
# java.util.Properties keeps all of those in the class name.
get_yaml_authenticator() {
local yaml="./conf/gremlin-server.yaml"

[[ -f "${yaml}" ]] || return 0
awk '
function scalar(s, out, i, n, c, q) {
out = ""
q = ""
n = length(s)
for (i = 1; i <= n; i++) {
c = substr(s, i, 1)
if (q != "") {
if (c == q) q = ""
else out = out c
continue
}
if (c == "\"" || c == "\047") { q = c; continue }
if (c == "#" &&
(out == "" || substr(out, length(out), 1) ~ /[ \t]/))
break
if (c == "," || c == "}" || c == "]") break
out = out c
}
sub(/^[ \t\r]+/, "", out)
sub(/[ \t\r]+$/, "", out)
return out
}
/^[ \t]*#/ { next }
/^[ \t]*authentication[ \t]*:/ {
inblk = 1
indent = match($0, /[^ \t]/)
line = $0
sub(/^[ \t]*authentication[ \t]*:[ \t]*/, "", line)
if (match(line, /authenticator[ \t]*:/)) {
print scalar(substr(line, RSTART + RLENGTH))
exit
}
next
}
# A blank line does not close a YAML mapping.
/^[ \t\r]*$/ { next }
# The authenticator has to belong to the authentication mapping:
# any key at or left of that key is a sibling, so the block is
# over. Without this, the first `authenticator:` anywhere below
# `authentication:` is taken as the Gremlin one, which lets a
# later top-level mapping carrying its own authenticator decide
# the REST side too.
inblk && match($0, /[^ \t]/) <= indent { inblk = 0 }
inblk && /^[ \t]+authenticator[ \t]*:/ {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ inblk is never cleared when the YAML mapping leaves authentication:. For example, after authentication: has no authenticator, a later other: authenticator: com.example.Other is accepted here and then written into REST by align_auth_config, although it is not authentication.authenticator and Gremlin still uses its default. Track the block indentation or use the production YAML parser, and cover a following top-level mapping.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b93b52e. Reproduced exactly as described: with the block open, the first authenticator: below authentication: wins no matter which mapping it belongs to, and align_auth_config then writes that class into rest-server.properties — so a class only ever mentioned to an unrelated mapping ends up authenticating REST.

The block now tracks the key's own column and closes on the next key at or left of it:

/^[ \t]*authentication[ \t]*:/ {
    inblk = 1
    indent = match($0, /[^ \t]/)
    ...
}
# A blank line does not close a YAML mapping.
/^[ \t\r]*$/ { next }
inblk && match($0, /[^ \t]/) <= indent { inblk = 0 }
inblk && /^[ \t]+authenticator[ \t]*:/ { ... }

The blank-line rule has to precede the closing rule, otherwise a CR-only "blank" line in a CRLF-saved yaml (column 1) would end the block early. Comment lines were already skipped by the existing /^[ \t]*#/ rule, and that rule stays ahead of the closer so a commented-out authentication: cannot close the block either.

Four cases added: sibling mapping after the block returns nothing; the block's own authenticator is still found when a sibling follows it; a blank line and a commented authenticator inside the block don't break it; and a block that is itself indented ( authentication: / authenticator:) still resolves, because the comparison is against the key's column rather than zero.

Against the two real layouts in the tree rather than fixtures — the entrypoint function sourced and run on the actual files:

conf-raft1 -> 'org.apache.hugegraph.auth.StandardAuthenticator'
conf       -> ''

conf-raft1 is the case worth checking: its block is the last thing in the file, line 122 inside it is a commented-out #authenticationHandler: (so the comment rule has to stay ahead of the closer), and the authenticator is on line 121 — it still resolves.

Deleting only the inblk = 0 rule makes the new case fail (EXIT=1), so the coverage is load-bearing.

line = $0
sub(/^[ \t]*authenticator[ \t]*:[ \t]*/, "", line)
print scalar(line)
exit
}
' "${yaml}"
}

# A mounted yaml can carry an authentication block whose authenticator
# cannot be read (an empty or unparseable one). That is not the
# both-empty case: exporting the default would override an explicit
# choice that snakeyaml does resolve, so callers treat it as a mismatch.
has_yaml_authentication_block() {
local yaml="./conf/gremlin-server.yaml"

[[ -f "${yaml}" ]] || return 1
grep -Eq '^[[:blank:]]*authentication[[:blank:]]*:' "${yaml}"
}

# enable-auth.sh appends definitions to files it did not write. On a
# mounted config those appended definitions are duplicates the two parsers
# resolve in opposite directions — HugeConfig (commons-configuration) takes
# the first, snakeyaml takes the last — so Gremlin and REST can land on
# different authenticators with no error from either. Normalize both sides
# to one definition of the same authenticator here; enable-auth.sh's
# per-file guards then make its appends no-ops on anything already set.
align_auth_config() {
local rest_auth yaml_auth

rest_auth=$(get_prop "auth.authenticator" "${REST_SERVER_CONF}")
yaml_auth=$(get_yaml_authenticator)
if [[ -z "${yaml_auth}" ]] && has_yaml_authentication_block; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This branch logs "leaving both sides untouched", but enable-auth.sh runs right after it and only touches the REST side.

At bedc21e, with a yaml block that has no authenticator and a rest-server.properties without auth.authenticator:

gremlin-server.yaml:
  authentication:
    authenticationHandler: org.apache.hugegraph.auth.WsAndHttpBasicAuthHandler
    config: {tokens: conf/rest-server.properties}

align_auth_config  -> WARN ... leaving both sides untouched (AUTHENTICATOR_CLASS unset)
./bin/enable-auth.sh
rest:  auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator
       auth.graph_store=hugegraph
yaml:  unchanged (its guard sees `authentication:`)
graph: gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy

In TinkerPop 3.5.1 Settings.AuthenticationSettings.authenticator defaults to AllowAllAuthenticator, so REST is on StandardAuthenticator and Gremlin is on AllowAllAuthenticator. That is the split this function is meant to prevent. Before this change a first run appended a second authentication: block, and snakeyaml's last-wins rule put both sides on the default. The test at test/test-docker-entrypoint.sh:618-625 stops at align_auth_config and never runs enable-auth.sh.

Requested change: make this branch keep the bootstrap from writing just one side. Skip enable-auth.sh here, fail the entrypoint, or add the default authenticator to the yaml block as well. Please also extend the test to run enable-auth.sh after this branch.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bf2718f: the branch now fails the entrypoint (ERROR + nonzero exit under set -e) instead of warning while enable-auth.sh writes REST alone. Chose fail-fast over skip/add-to-yaml: skipping would start a PASSWORD deployment with auth silently half-applied, and appending an authenticator line into YAML by shell surgery risks corrupting the user's block. The unit test now asserts the refusal and runs enable-auth.sh against the same layout to prove it would write only REST (yaml untouched) — i.e. what the refusal prevents. Both shell suites pass locally.

# Refuse instead of bootstrapping one side: enable-auth.sh runs right
# after align and only touches the REST side, so continuing would put
# REST on StandardAuthenticator while Gremlin stays on TinkerPop's
# AllowAllAuthenticator default. Failing fast (rather than skipping
# enable-auth.sh) keeps a PASSWORD deployment from starting with
# authentication silently half-applied.
log "ERROR: gremlin-server.yaml carries an authentication block" \
"without a readable authenticator; refusing to bootstrap" \
"authentication one-sided. Add an 'authenticator:' entry to" \
"the block or remove the block, then restart."
return 1
fi
if [[ -n "${rest_auth}" && -n "${yaml_auth}" && "${rest_auth}" != "${yaml_auth}" ]]; then
Comment thread
Adarsh-Me marked this conversation as resolved.
log "WARN: REST and Gremlin name different authenticators" \
"('${rest_auth}' vs '${yaml_auth}'); leaving both untouched"
return
fi
if [[ -z "${rest_auth}" && -z "${yaml_auth}" ]]; then
# Only fill in a default: an operator-supplied AUTHENTICATOR_CLASS
# is the intent for a config that names no authenticator yet, and
# assigning here would turn it back into StandardAuthenticator
# before enable-auth.sh ever saw it.
export AUTHENTICATOR_CLASS="${AUTHENTICATOR_CLASS:-org.apache.hugegraph.auth.StandardAuthenticator}"
elif [[ -n "${yaml_auth}" ]]; then
set_prop "auth.authenticator" "${yaml_auth}" "${REST_SERVER_CONF}"
else
export AUTHENTICATOR_CLASS="${rest_auth}"
fi
# auth.graph_store and the gremlin.graph flip are left to enable-auth.sh,
# which appends/rewrites only what is absent or still the plain default.
}

migrate_env() {
Expand Down Expand Up @@ -147,6 +280,9 @@ elif [[ -n "${AUTH_TOKEN_SECRET_ENCODED}" ]]; then
fi
if [[ -n "${PASSWORD:-}" ]]; then
set_prop "auth.admin_pa" "${PASSWORD}" "${REST_SERVER_CONF}"
# A refusal inside align_auth_config exits the entrypoint under set -e,
# so enable-auth.sh can never run one-sided after it.
align_auth_config
# This script is idempotent and must run outside the initialization guard:
# an upgrade can preserve the marker from an unauthenticated deployment.
./bin/enable-auth.sh
Expand Down
Loading