Skip to content

fix(docker): make auth bootstrap safe for mounted and upgraded configs - #3192

Open
Adarsh-Me wants to merge 21 commits into
apache:masterfrom
Adarsh-Me:fix-entrypoint-auth-bootstrap
Open

Adarsh-Me wants to merge 21 commits into
apache:masterfrom
Adarsh-Me:fix-entrypoint-auth-bootstrap

Conversation

@Adarsh-Me

@Adarsh-Me Adarsh-Me commented Sep 3, 2026 •

Copy link
Copy Markdown

What is changing

Closes #3133 (the parts still open on master 3681148, since #3119 landed the rest).

Properties rewriting now implements the Java grammar. docker-entrypoint.sh previously rewrote rest-server.properties / hugegraph.properties with grep/sed, which disagrees with HugeConfig on mounted or upgraded configs: backslash-escaped keys, :/whitespace separators, line continuations, and duplicate definitions are all parsed differently. The property logic moves to a new props.awk loaded by the entrypoint, which implements the java.util.Properties line grammar (comments, both separators, continuations, backslash escapes, first-definition-wins duplicates) and rewrites the first definition in place while keeping every untouched line byte-for-byte.

PASSWORD no longer appears in ps output. The old sed rewrite interpolated the encoded value into sed's command line, so when a key already existed (mounted or persisted config) the password was visible in ps. Values now travel through an environment variable into awk, never through argv.

enable-auth.sh appends are per-file guarded. The old script appended authentication definitions whenever conf-bak/ was absent. On a config it did not write, that created duplicate definitions that the properties parser (first definition wins) and snakeyaml (last definition wins) resolved in opposite directions — Gremlin and REST could land on different authenticators with no error from either. Now each append runs only when its file lacks the definition (or still has it commented out), re-runs are idempotent, custom gremlin.graph factories are preserved, and the authenticator class can be overridden via AUTHENTICATOR_CLASS.

The entrypoint aligns both sides before enabling auth. If the yaml declares an authenticator but the properties file does not (or vice versa), the entrypoint propagates it to the other side instead of letting the default StandardAuthenticator split the pair. When both sides name genuinely different authenticators, it logs a WARN and leaves both untouched instead of silently splitting them.

Implementation notes

  • I chose a shell/awk implementation of the properties grammar over the issue's proposed Java ConfigTool CLI: it delivers the same parser-agreement contract with a much smaller footprint and no new build artifact. Happy to rework toward the ConfigTool if reviewers prefer that direction.
  • props.awk ships in both server images (Dockerfile COPY) and in the test sandbox; CI already runs the unit suite via docker-build-ci.yml.

How was this tested

  • docker/test/test-docker-entrypoint.sh extended with cases for: escaped-key definitions rewritten in place, continuation lines consumed with the key they belong to, get-mode separator/continuation/duplicate semantics, and appends when the key only exists commented out. All pass.
  • docker-entrypoint-test.sh full harness passes end-to-end (secret round-trips incl. backslash/space/trailing-space secrets, enable-auth call counting unchanged).
  • enable-auth.sh manually exercised against: fresh default config, idempotent re-run, mounted config with custom authenticator (no duplicates), and custom gremlin.graph factory (preserved).

Code Review Handbook

  • props.awk is the core: block model (comment lines and logical entries), first-definition-wins, raw-value round-trip (get returns the on-disk escaped form so feeding it back into set is byte-exact).
  • The value of get is intentionally not unescaped: the entrypoint re-writes secrets it just read, and unescape-then-re-encode would double-escape backslashes (caught by the complex-secret round-trip in docker-entrypoint-test.sh).
  • The escaped-key unit case is the reported auth\.admin_pa scenario: the old grep could not match it, so the append created a duplicate and HugeConfig silently kept pa.

Visual summary

Docker auth bootstrap

The entrypoint's grep/sed property rewriting disagrees with HugeConfig
on mounted or upgraded configs: escaped keys, ':'/whitespace
separators, line continuations, and duplicate definitions are all read
differently, so a mounted config could end up with two logical
definitions of one key.  Property reading/writing now goes through
props.awk, which implements the java.util.Properties grammar
(comments, both separators, continuations, backslash escapes,
first-definition-wins duplicates) and keeps every untouched line
byte-for-byte.  Values travel through environment variables instead of
command arguments, so a PASSWORD no longer shows up in 'ps' output
when a key is rewritten in place.

enable-auth.sh appended authentication definitions whenever conf-bak/
was absent, which on a mounted config created duplicate definitions
that the properties parser (first definition wins) and the yaml parser
(last definition wins) resolved in opposite directions -- Gremlin and
REST could land on different authenticators with no error from either.
Its appends are now guarded per file, only an absent or still
commented-out definition triggers an append, re-runs are idempotent,
and the authenticator class is overridable through AUTHENTICATOR_CLASS.
The entrypoint aligns both sides before calling it: it copies a yaml
authenticator into rest-server.properties, or exports the REST one for
the yaml append, and warns without touching anything when the two name
genuinely different authenticators.

The unit test suite covers escaped keys, continuations, get-mode
semantics, and comment-guarded appends; the entrypoint harness now
ships props.awk into its sandbox, and both server Dockerfiles COPY it
next to the entrypoint.

Fixes apache#3133

@bitflicker64 bitflicker64 left a comment

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.

Blocking: yes. Summary: props.awk is careful work and both shell suites this PR touches pass under the image's own mawk, but the new alignment layer does not hold on the mounted configs the PR targets. The yaml authenticator is copied into rest-server.properties without unquoting, a flow-style authentication: block reads as absent so REST silently falls back to the default, enable-auth.sh's guards accept only the key= spelling so duplicates are still appended, and the narrowed gremlin.graph guard no longer converts a CRLF config the old sed did convert. Two doc fixes as well. Evidence: measured at 698b0c3 in ubuntu:22.04 (GNU grep 3.7, GNU sed, mawk 1.3.4), the same toolchain eclipse-temurin:11-jre-jammy ships; test/test-docker-entrypoint.sh and docker-entrypoint-test.sh both exit 0 there; each finding below quotes the config the run produced. All seven workflow runs on this head are action_required and the combined status is pending with zero statuses, so there is no CI evidence for the image builds.

Comment thread hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh Outdated
Comment thread hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh Outdated
Comment thread hugegraph-server/hugegraph-dist/src/assembly/static/bin/enable-auth.sh Outdated
Comment thread hugegraph-server/hugegraph-dist/src/assembly/static/bin/enable-auth.sh Outdated
Comment thread hugegraph-server/hugegraph-dist/docker/props.awk Outdated
Comment thread hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh Outdated

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: The new properties parser still mishandles valid indented keys, so mounted authentication values can remain stale after an environment override. Evidence: reproduced at the exact head with the PR helper; existing current-head comments cover other findings and are not duplicated.

Comment thread hugegraph-server/hugegraph-dist/docker/props.awk Outdated
@codecov

codecov Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 41.35%. Comparing base (83ef9f3) to head (c1dde5e).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3192   +/-   ##
=========================================
  Coverage     41.35%   41.35%           
- Complexity     7299     7313   +14     
=========================================
  Files           802      802           
  Lines         69688    69770   +82     
  Branches       9291     9309   +18     
=========================================
+ Hits          28816    28850   +34     
- Misses        37576    37627   +51     
+ Partials       3296     3293    -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Review follow-ups on the props.awk bootstrap: the yaml authenticator
scalar now goes through a snakeyaml-shaped cleanup (inline comments,
quotes and padding stripped) instead of only cutting at the first comma
or colon; a flow mapping on the authentication line itself is read, and
an authentication block without a readable authenticator takes the WARN
branch instead of the both-empty default.  props.awk strips leading
whitespace before the key the way java.util.Properties does, so an
indented key is rewritten in place rather than duplicated.

enable-auth.sh's append guards now accept the ':', bare-whitespace and
backslash-escaped spellings with [[:blank:]] classes (the '[ \t]'
bracket matched space, backslash and the letter t), and the
gremlin.graph flip embeds the carriage return as a byte because GNU
grep reads \r in a pattern as the letter r, which made the anchored
guard drop mounted CRLF configs.  Test docs name the environment
variables and the function count they rely on, and new regression tests
cover indented keys, yaml scalar cleanup, flow mappings and the
block-without-authenticator WARN.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: The fresh-default path works and is idempotent at this head (one auth.authenticator, one authentication: block, gremlin.graph flipped to HugeFactoryAuthProxy, stable across three re-runs), and the fixes for the earlier review round all check out. Four gaps remain between what the new code promises for mounted or upgraded configs and what it does, three of them around CRLF and separator spellings that java.util.Properties accepts. Evidence: entrypoint helpers extracted the way test/test-docker-entrypoint.sh does, run against verbatim head copies of props.awk and bin/enable-auth.sh with GNU sed; props.awk output compared byte for byte against java.util.Properties.load on the same files; the gremlin.graph guard and its sed run against eight legal spellings. Not covered: no Docker daemon on this host, so the images were not built and the runtime mawk path was not exercised. props.awk uses only POSIX awk features and awk is already a dependency of the shipped bin/*.sh, but CI's docker-build-ci.yml run of the suite remains the authority there.

Comment thread hugegraph-server/hugegraph-dist/docker/props.awk Outdated
Comment thread hugegraph-server/hugegraph-dist/docker/props.awk Outdated
Comment thread hugegraph-server/hugegraph-dist/src/assembly/static/bin/enable-auth.sh Outdated
Comment thread hugegraph-server/hugegraph-dist/docker/docker-entrypoint.sh Outdated
Address review 5185689081 on the auth bootstrap alignment:

- props.awk: strip one trailing CR while assembling logical lines so
  CRLF configs parse like java.util.Properties, without touching the
  RAW bytes replayed on rewrite; add get-decoded mode.
- props.awk: die when getline fails and rewrite atomically through a
  sibling temp file renamed over the original.
- enable-auth.sh: widen the gremlin.graph guard and flip together for
  colon, equals, bare-whitespace, leading-blank and escaped-dot
  spellings with optional CR, still skipping proxied/commented lines.
- docker-entrypoint.sh: compare the unescaped authenticator with the
  yaml scalar and write the yaml side through the encoding setter.

Add CRLF plus escaped-authenticator regression cases to
test-docker-entrypoint.sh.
@Adarsh-Me

Copy link
Copy Markdown
Author

All 4 inline threads of review 5185689081 addressed in 5f50511 (branch fix-entrypoint-auth-bootstrap): (A) CRLF stripped while assembling logical lines, RAW replay untouched + regression test; (B) die on getline -1 plus atomic sibling-temp rewrite with quoted rename; (C) gremlin.graph guard and sed widened together (colon/equals/bare-whitespace, leading blank, escaped dot, optional CR), proxied/commented still skipped, CR preserved; (D) decoded authenticator comparison via new get-decoded mode with encoding setter on write. Verification: bash test-docker-entrypoint.sh passes (exit 0), bash -n clean on all three shell files, guard/sed exercised against 8 spellings + CRLF/proxied/commented cases. shellcheck not installed on this host, so that step was skipped. No Docker daemon here, so image build/mawk paths remain for CI (docker-build-ci.yml).

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: The earlier review rounds are addressed at this head: CRLF parsing, the unreadable-file guard, the widened gremlin.graph guard and sed, and the decoded authenticator comparison all check out, and the unit suite passes. One new problem comes from the atomic rewrite added in this round. props_set now swaps in a new file with mv, so the rewritten config loses its original permissions and any symlink, and a config bind-mounted as a single file can no longer be updated at all. Evidence: bash hugegraph-server/hugegraph-dist/docker/test/test-docker-entrypoint.sh exits 0 at 5f50511. A direct PROPS_MODE=set run against a 0600 file left it 0644 with auth.admin_pa in it, and a symlinked config was replaced by a regular file. The bind-mount failure follows from rename(2) returning EBUSY when the target is a mount point; there is no Docker daemon on this host, so that case was not run. CI has not run yet: all seven workflow runs on this head are action_required.

Comment thread hugegraph-server/hugegraph-dist/docker/props.awk Outdated
The staged temp file was renamed over the config, replacing its inode:
a 0600 config holding secrets came back umask-world-readable, a
symlinked config was replaced by a regular file, and a config
bind-mounted as a single file could not be renamed over at all
(rename(2) returns EBUSY on a mount point), aborting the entrypoint on
exactly the mounted configs this path exists for.

The temp file is now copied back onto the original instead, which
keeps the inode, mode, symlink and mount point, and is created 0600
itself since it can hold secrets while it exists.  Regression tests
check that a 0600 file keeps its mode and that a symlink survives a
set with its target rewritten.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: The fixes from the earlier rounds are in place at this head. One gap is left: when the yaml has an authentication: block with no readable authenticator, the WARN branch says it leaves both sides alone, but enable-auth.sh still writes the REST authenticator, so REST and Gremlin end up on different authenticators. There is also a small ordering problem with the temp-file mode in props.awk. Evidence: helpers pulled from docker-entrypoint.sh the way test/test-docker-entrypoint.sh does it, run against head copies of props.awk and bin/enable-auth.sh; TinkerPop 3.5.1 default checked with javap on Settings$AuthenticationSettings. Not covered: images not built and the suite not run under mawk (no Docker daemon here). All seven workflow runs on bedc21e are action_required, so CI has not run.


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.

# symlink and mount point, and since the temp file is fully written
# before the original is truncated, a failed copy still leaves the
# previous content on disk.
system("chmod 600 -- " shquote(tmp))

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.

🧹 The mode is set after the secret has already been written. The comment on line 210 says the temp file "is created 0600 regardless of the umask", but lines 215 and 222 create tmp with awk's > under the process umask (usually 0644), and this chmod only runs after close(tmp). Until then auth.admin_pa or auth.token_secret sits in a group- and world-readable file.

Requested change: create the file 0600 before the first write, for example system("umask 077 && : > " shquote(tmp)) ahead of the loop (awk's > then truncates it and keeps the mode), or correct the comment.

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 along the suggested lines: props_set pre-creates the tmp file with umask 077 && : > tmp before the first write, so secrets never sit umask-readable; the chmod after close is kept to repair a stale tmp left by a crashed run. Both shell suites pass locally.

Unreadable-authenticator yaml block: align_auth_config now fails the
entrypoint instead of logging 'leaving both sides untouched' while
enable-auth.sh goes on to write the REST side alone (REST on
StandardAuthenticator vs Gremlin on AllowAllAuthenticator). The error tells
the operator to add an 'authenticator:' entry or remove the block.

props.awk: pre-create the rewrite temp file 0600 (umask 077) before the
first write so secrets never sit briefly umask-readable; the chmod after
close is kept for stale tmp files from crashed runs.

Tests: the unreadable-block case now asserts refusal, and a new case runs
enable-auth.sh against the same layout to prove it would write only REST
(yaml untouched, graph flipped) — i.e. what the refusal prevents.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: yes. Summary: The earlier review rounds are addressed at this head, and the fail-fast refusal plus the 0600 pre-create in props.awk both look right. But the one-sided regression test added in bf2718f fails, so Docker Build CI / docker-build (hugegraph-server/Dockerfile) is red on this head. The test seeds an empty rest-server.properties, and GNU sed '$a\...' appends nothing to an empty file. Evidence: the failed job log for run 35101931936 stops right after the second refusal ERROR, inside the yaml-onesided block. Locally, GNU sed 4.10 left a zero-byte file untouched after sed -i -e '$a\auth.authenticator=...', and the same enable-auth.sh run against the test layout left rest-server.properties empty, so the grep -q '^auth\.authenticator=...StandardAuthenticator$' assertion exits 1. Not covered: the full suite was not run under the image's mawk/GNU toolchain (no Docker daemon here).

(
cd "${onesided_dir}" || exit 1
REST_SERVER_CONF="./conf/rest-server.properties"
: > conf/rest-server.properties

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 empty fixture makes the new one-sided test fail, which is why docker-build (hugegraph-server/Dockerfile) is red at bf2718f.

enable-auth.sh appends with sed -i -e '$a\...'. On an empty file GNU sed has no last line, so $a never runs and nothing is written. The grep -q '^auth\.authenticator=org\.apache\.hugegraph\.auth\.StandardAuthenticator$' on line 221 then exits 1 and set -e stops the suite. Checked with GNU sed 4.10:

$ : > empty.properties
$ sed -i -e '$a\auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator' empty.properties
$ wc -c < empty.properties
0

In CI (run 35101931936), the log stops right after the second refusal ERROR, which comes from this block.

Requested change: seed the fixture with one unrelated line so it matches a real rest-server.properties, for example printf '%s\n' 'restserver.url=http://0.0.0.0:8080' > conf/rest-server.properties. If an empty mounted file should also work, switch the two REST appends in enable-auth.sh to printf '%s\n' ... >> "${CONF}/${REST_SERVER_CONF}", which also writes to an empty file.

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.

Confirmed, and the fixture turned out to be the least interesting part of this — the empty file is a real input the entrypoint supports (props.awk happily appends to a zero-byte config, and auth.admin_pa lands in it), so I fixed enable-auth.sh rather than seeding the fixture. Seeding it would have made CI green while leaving the bug in.

Root cause, isolated on GNU sed 4.9:

$ printf '' > empty.txt
$ sed -i -e '$a\hello=world' empty.txt; echo "exit=$?"   # exit=0
$ wc -c < empty.txt                                       # 0

$ never matches without at least one line, so every append in the script was a silent no-op on an empty config. Run against the script as of bf2718f with an empty rest-server.properties and an empty gremlin-server.yaml:

enable-auth rc=0
rest bytes: 0
yaml bytes: 0
1                     # gremlin.graph -> HugeFactoryAuthProxy

That is the shape worth the ⚠️: exit code 0, the factory flipped to the authenticating proxy, and neither server told to authenticate. The entrypoint has already written auth.admin_pa and run init-store in auth mode by the time this returns, so a deployment that asked for auth serves unauthenticated and reports success.

The appends now go through a helper that writes with >> and first closes a missing trailing newline (sed -i '$a' used to do that for us, >> does not):

append_lines() {
    local file="$1"
    shift
    if [[ -s "${file}" && -n "$(tail -c 1 "${file}")" ]]; then
        printf '\n' >> "${file}"
    fi
    printf '%s\n' "$@" >> "${file}"
}

Same fixture, same command, after the change:

enable-auth rc=0
rest:
auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator
auth.graph_store=hugegraph
yaml:
authentication: {
  authenticator: org.apache.hugegraph.auth.StandardAuthenticator,
  ...
}

The new case keeps the empty fixture and asserts all of it, plus a re-run for idempotence and a config whose last line has no terminator. Reverting only enable-auth.sh reproduces the original EXIT=1 at line 221, so docker-build (hugegraph-server/Dockerfile) on run 35101931936 should go green.

Two things I cannot claim from here, so you know what this CI run is actually first-testing:

  • My host is Git Bash, and MSYS stdio is text mode — awk and grep never see a CR byte, chmod is a no-op (stat -c %a reports 644 for a 600 file), and ln -s writes a copy. So the CRLF case (lines 230-249), the mode-preservation case and the symlink case cannot be executed here; I ran the file with exactly those four assertions neutralised and it exits 0. They are correct assertions, they are just untestable on Windows.
  • Because the script died at line 221, everything after it has never run in CI either — this push is the first execution of lines 224 onward, including the four cases I added. If any of them is red I will get to it immediately.

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: The current head still has auth bootstrap correctness issues for custom and mounted configurations, while the Docker Build CI failure is already covered by the existing exact-head inline finding. Evidence: exact-head static verification against docker-entrypoint.sh, props.awk, enable-auth.sh, and the failed docker-build (hugegraph-server/Dockerfile) check in run 35101931936.

return
fi
if [[ -z "${rest_auth}" && -z "${yaml_auth}" ]]; then
export AUTHENTICATOR_CLASS="org.apache.hugegraph.auth.StandardAuthenticator"

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.

⚠️ This branch overwrites an operator-supplied AUTHENTICATOR_CLASS. When both REST and YAML have no authenticator, it unconditionally exports StandardAuthenticator before enable-auth.sh reads the variable, so a fresh deployment with AUTHENTICATOR_CLASS=com.example.CustomAuthenticator silently uses the default. Preserve a non-empty environment value (only default when unset) and add a fresh-config regression case.

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:

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}"

AUTHENTICATOR_CLASS is the only channel a caller has for naming a class — it is what enable-auth.sh interpolates into the yaml block and what the other two branches of this if export — and on a mounted config that names no authenticator anywhere this branch is the only one that ever runs, so the value could not survive a start with PASSWORD set.

Left deliberately asymmetric: the else branch still exports rest_auth over the caller's value. There the config does name a class, and propagating the file is the whole point of align_auth_config; a value already in rest-server.properties beats an env var, and enable-auth.sh's per-file guard would refuse to append a second definition anyway. The both-empty branch is the only one where the env var is the sole signal.

New case covers both directions — an operator value survives, and with the variable unset the default still lands:

AUTHENTICATOR_CLASS=com.example.OperatorAuth → com.example.OperatorAuth
unset AUTHENTICATOR_CLASS                    → org.apache.hugegraph.auth.StandardAuthenticator

Restoring the unconditional assignment makes the first assertion fail (EXIT=1).

}
next
}
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.

# before the original is truncated, a failed copy still leaves the
# previous content on disk.
system("chmod 600 -- " shquote(tmp))
cmd = "cat -- " shquote(tmp) " > " shquote(file) " && rm -f -- " shquote(tmp)

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.

‼️ The copy-back is not failure-safe despite the comment above it. cat -- tmp > file truncates the original before the copy completes, so an interruption, ENOSPC, or I/O error can leave a mounted rest-server.properties partially written; system() then reports the error but cannot restore the old contents. Preserve the old file on copy failure (for example with a tested backup/rollback path for inode-preserving mounts) and add failure-injection coverage.

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. You are right, and the comment above it was the part that was wrong: cat -- tmp > file truncates the destination in the shell before cat runs, so an error mid-copy left a partial file and the "previous content is still on disk" claim did not hold.

The copy-back is now preceded by a snapshot and followed by a repair:

bak = file ".bak"
cmd = "umask 077 && cp -- " shquote(file) " " shquote(bak)
if (system(cmd " && chmod 600 -- " shquote(bak)) != 0)
    die("cannot back up " file " before the copy-back")
cmd = "cat -- " shquote(tmp) " > " shquote(file)
if (system(cmd) != 0) {
    msg = "cannot copy " tmp " over " file
    cmd = "cat -- " shquote(bak) " > " shquote(file)
    if (system(cmd) == 0) die(msg "; the previous content is restored")
    die(msg "; " file " is damaged, previous content is in " bak)
}
if (system("rm -f -- " shquote(tmp) " " shquote(bak)) != 0)
    die("cannot remove " tmp " and " bak " after the copy-back")

Notes on the choices:

  • Still a copy, not a rename, so the inode / mode / symlink / bind-mount properties the previous commit added stay true.
  • umask 077 plus the explicit chmod 600 means a backup taken of a 0644 mounted config is never more permissive than its source, and repairs a stale .bak from a crashed run the same way the tmp path already does. The snapshot can hold auth.admin_pa in clear.
  • Both staging files survive a failure on purpose: the .tmp is what was being written and the .bak is the way back. The success path removes both, which is now asserted.

Evidence — awk -f props.awk driven by the entrypoint's set_prop, with the staging cat replaced through PATH so the copy fails the way ENOSPC would (stdout is the already-truncated destination, so the fake writes 23 bytes and exits 1):

fake cat invoked:  -- .../config-rollback.tmp
                   -- .../config-rollback.bak
RESULT: set_prop failed as intended
--- config after rollback ---
auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator
auth.token_secret=s3cr3t
unrelated=true
MATCH: content restored

Two cases carry this; bf83032 adds the second one: the restore path above, and the case where the restore also fails — then the config is left damaged (0 bytes, measured; the honest limit of a copy that cannot be made atomic without replacing the inode), props.awk names <file>.bak in its error, and the snapshot is asserted byte-identical to the original. Reverting only props.awk to bf2718f makes the first case fail with a failed copy-back must leave the previous content in place, so the test does discriminate.

Four review findings on the auth bootstrap, each with a case that fails
without the change:

- enable-auth.sh: the `sed -i '$a\...'` appends were silent no-ops on a
  file with no lines, so an empty mounted config received neither
  `auth.authenticator` nor the yaml `authentication:` block while the
  entrypoint had already applied PASSWORD and init-store had run in auth
  mode.  Append with `>>`, closing a missing trailing newline first.
  This is the failure that red `docker-build
  (hugegraph-server/Dockerfile)` reports on run 35101931936.
- props.awk: `cat tmp > file` truncates the destination before cat
  writes, so a mid-copy failure (ENOSPC, EIO) left a half-written config
  on disk rather than the previous content.  Snapshot the original under
  umask 077 first, restore it when the copy fails, and only drop both
  staging files once the copy has succeeded.
- docker-entrypoint.sh: get_yaml_authenticator opened its block on
  `authentication:` and never closed it, so an `authenticator:` belonging
  to any later mapping was read as Gremlin's and then written into the
  REST config.  Track the key's indentation and end the block at the
  next key at or left of it.
- docker-entrypoint.sh: the both-empty branch assigned the default
  authenticator unconditionally, discarding an operator-supplied
  AUTHENTICATOR_CLASS before enable-auth.sh could use it.  It now only
  fills the value in when unset.
The injected `cat` can fail the copy-back and the restore at once, which
is the only case where props.awk cannot repair the config.  Assert the
operator is pointed at the snapshot, that the snapshot is a byte-for-byte
copy of what was there before, and that both staging files are left
behind.
@Adarsh-Me

Copy link
Copy Markdown
Author

One correction to what I wrote in the empty-fixture thread: I said docker-build (hugegraph-server/Dockerfile) "should go green", and it cannot tell us anything yet — CI has not run on either new commit. Every workflow on b93b52e and bf83032 is sitting at action_required, i.e. GitHub is holding the runs for this fork PR until a committer approves them (Docker Build CI on bf83032). Could someone with write access approve the workflows? The last approved run is 35101931936, which failed at test-docker-entrypoint.sh:221 and never reached anything after it.

Where the four open findings landed:

finding fix case that fails without it
copy-back is not failure-safe (props.awk:240) b93b52e — 0600 snapshot before the copy, restore on failure, staging files removed only on success injected cat fails the copy; cmp against a pristine copy
inblk never cleared (docker-entrypoint.sh:139) b93b52e — block closes at the next key at or left of authentication:'s column sibling mapping after the block must return nothing; also checked against the shipped conf-raft1 yaml
AUTHENTICATOR_CLASS overwritten (docker-entrypoint.sh:190) b93b52e — default fills in only when unset operator value survives / unset still defaults
empty fixture reds the build (test-docker-entrypoint.sh:207) b93b52e — the appends move off sed -i '$a\...', which cannot match an empty file; the fixture is kept because the empty config is a real input the empty-config case, plus the original line-221 assertion

What I could not execute here, so the approved run is the first real signal for it: my host is Git Bash, and MSYS stdio is text mode (awk/grep never see a CR byte), chmod is a no-op (stat -c %a says 644 for a 600 file) and ln -s writes a copy instead of linking. The CRLF case, the mode-preservation case and the symlink case therefore had to be neutralised to run the file locally — they exit 0 with those four assertions removed. Everything else, including all four cases added for the findings above, runs and passes locally, three times in a row.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: The parser premise holds, HugeConfig extends commons-configuration2 PropertiesConfiguration and props.awk agrees with it on escaped keys, continuations, indented keys, bare-whitespace separators and the first-definition-wins duplicate rule; four findings remain, two of which can misconfigure or fail a mounted deployment. Evidence: probed commons-configuration2-2.8.0 and java.util.Properties side by side against props.awk on the same fixtures, and reproduced each finding through the entrypoint's own helpers at bf83032; both shell suites pass at this head. Not verified: CI has not run at this head (no check runs, combined status pending, workflows held at action_required), and props.awk was not exercised under the mawk the runtime image ships.

}
start = nl
logical = stripped
while (trailing_backslashes(logical) % 2 == 1 && nl < NLINES) {

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.

⚠️ A value ending in an odd number of backslashes on the file's last line keeps that backslash, and writing it back makes the next line part of that value.

The continuation loop stops at nl < NLINES, so the trailing backslash survives into V_RAW. Measured at this head against commons-configuration2-2.8.0, the library HugeConfig extends:

rest-server.properties, last line:  auth.token_secret=abc\

props.awk  get         -> [abc\]
cc2 2.8.0  getString   -> [null]    (property dropped entirely)
JDK Props  getProperty -> [abc]

Line 243 reads that value and lines 275-279 write it into conf/graphs/hugegraph.properties, where it is no longer the last line:

BEFORE                          AFTER                           re-read
gremlin.graph=...HugeFactory    gremlin.graph=...HugeFactory
auth.token_secret=old           auth.token_secret=abc\          [abcbackend=rocksdb]
backend=rocksdb                 backend=rocksdb                 []
serializer=binary               serializer=binary

The bytes of the untouched lines are preserved as promised, but backend is now a continuation of the secret, so the server sees no backend property at all and neither side reports anything.

Requested change: have props_set refuse a value whose encoded form ends in an odd number of backslashes. Matching java.util.Properties here (dropping the backslash to yield abc) would be the wrong target: cc2 reads no property at all for that input, so the entrypoint would propagate a secret the server never had.

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 b8801a6 as requested — props_set now refuses rather than picking a target:

    nbs = 0
    while (nbs < length(enc_val) && substr(enc_val, length(enc_val) - nbs, 1) == "\\")
        nbs++
    if (nbs % 2 == 1)
        die("refusing to write " key ": the encoded value ends in an odd number of backslashes")

Agreed on the target being cc2 rather than java.util.Properties: matching the JDK here would drop the backslash and hand the server a secret that differs from the one on disk, which is worse than not writing it. The refusal is the only answer that cannot be wrong.

Covered by a new case in test-docker-entrypoint.sh that also asserts the config is left byte-for-byte untouched after the refusal, and that an escaped backslash (two of them, even) is still writable and replays unchanged. Checked by reverting the guard: the suite goes red on its own.

Note this arrives as a side effect of the other thread — get-decoded is gone, so the unescape path that produced your abc\ reading no longer exists for a write, but the hazard was in replaying untouched bytes, which props_set still does, so the refusal is what closes it.

return out
}
/^[ \t]*#/ { next }
/^[ \t]*authentication[ \t]*:/ {

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.

⚠️ authentication: is matched at any indentation, so a mapping nested under another key is taken as the Gremlin server's authentication block.

Line 166 has the same loose match. Run against the extracted helpers at this head:

someFeature:
  authentication:
    authenticator: com.example.Nested
get_yaml_authenticator -> com.example.Nested
rest-server.properties after align_auth_config:
  restserver.url=http://0.0.0.0:8080
  auth.authenticator=com.example.Nested

A class the operator only ever gave to an unrelated nested mapping now authenticates REST, while Gremlin stays on TinkerPop's AllowAllAuthenticator default. The same nested mapping without a readable authenticator: reaches has_yaml_authentication_block and fires the new refusal, so the container fails to start:

ERROR: gremlin-server.yaml carries an authentication block without a readable authenticator ...
(rc=1, entrypoint exits under set -e)

This is the sibling of the thread the inblk indent tracking fixed: that change scopes the block once it is open, but never checks that the key itself is top level. The test at test/test-docker-entrypoint.sh deliberately accepts an indented authentication:, so the two requirements conflict and need a decision.

Requested change: either require the key at column 0 (dropping the indented-block case), or track the parent path so an authentication: under another key is not treated as the Gremlin one.

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 b8801a6, taking your first option: the key must start at column 0.

        /^authentication[ \t]*:/ {
            inblk = 1
            have = 1

so the layout you ran no longer counts as the Gremlin mapping at all:

someFeature:
  authentication:
    authenticator: com.example.Nested

→ none, which means check_auth_sides sees neither side configured and lets enable-auth.sh write both from its own default, instead of putting com.example.Nested into rest-server.properties or refusing the boot.

On the conflict you spotted — yes, and this is the decision: the indented-authentication: case is dropped. The old test asserted authentication: opened the block, and that assertion cannot coexist with rejecting a nested mapping, since the two are the same bytes. conf/gremlin-server.yaml as shipped puts the key at column 0, and TinkerPop's own settings loader resolves it as a top-level key, so column 0 is the faithful reading and the indented case was this script being more permissive than the thing it is modelling. That test is removed; the nested case is asserted instead, both in the committed suite and as a want_state none line.

props_load(file)
for (b = 1; b <= NBLOCK; b++) {
if (BTYPE[b] == "entry" && BKEY[b] == key) {
print unescape(BVAL[b])

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.

🧹 get-decoded unescapes but does not trim, so a trailing space still reads as a mismatch.

commons-configuration2 trims the value; props.awk keeps trailing whitespace, which is java.util.Properties behaviour rather than the server's. Measured at this head with auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator followed by three spaces:

cc2 2.8.0 getString   -> [org.apache.hugegraph.auth.StandardAuthenticator]
props.awk get-decoded -> [org.apache.hugegraph.auth.StandardAuthenticator   ]

With gremlin-server.yaml naming the same class, align_auth_config then logs (one line, wrapped here):

WARN: REST and Gremlin name different authenticators ('org.apache.hugegraph.auth.StandardAuthenticator   ' vs 'org.apache.hugegraph.auth.StandardAuthenticator'); leaving both untouched

Same symptom as the escape thread this mode was added for, different mechanism. Nothing breaks because both sides do name the same class, but the WARN is wrong and the alignment is skipped.

Requested change: strip trailing whitespace in the get-decoded path so the comparison matches what HugeConfig reads.

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.

Moot in b8801a6, in the direction of your bigger thread on this file: get-decoded is deleted.

The mode existed so the decoded REST value could be compared against a decoded yaml scalar. Once the entrypoint stops reading which class the yaml names, there is no cross-file comparison left to trim for — check_auth_sides only asks whether auth.authenticator is present, and presence is not affected by trailing whitespace.

So the trailing-space mismatch cannot happen any more rather than being fixed: no WARN: REST and Gremlin name different authenticators, because there is no longer anything that compares the two classes. props_get_decoded and the unescape-based read are gone; unescape itself stays, since keys are still unescaped for matching (auth\.authenticator is the same key), which is now asserted directly by a new case.

# 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() {

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.

🧹 Worth a follow-up rather than a change here: this new reader is not used for the backend read at line 359, which still has the grep shape the PR replaces everywhere else.

ACTUAL_BACKEND=$(grep -E '^[[:space:]]*backend[[:space:]]*=' "${GRAPH_CONF}" | head -n 1 | sed 's/.*=//' | tr -d '[:space:]' || true)

It accepts only the = separator, so with HG_SERVER_BACKEND unset (when it is set, line 259 rewrites the entry as backend=... and the grep works) a mounted graph config using the other separators is missed:

conf/graphs/hugegraph.properties: 'backend : hstore'
  cc2 2.8.0 getString     -> [hstore]
  line 359 ACTUAL_BACKEND -> []

The hstore wait-partition.sh stabilization check is then skipped with no message.

Note it cannot simply become get_prop "backend" "${GRAPH_CONF}" while the trailing-whitespace gap above is open: backend=hstore with trailing spaces would return them and fail the == "hstore" test that tr -d '[:space:]' currently survives. The two belong together, or a TODO here pointing at it.

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 b8801a6, and it came along with the get-decoded removal rather than needing the trailing-whitespace gap closed first:

ACTUAL_BACKEND=$(get_prop_encoded "backend" "${GRAPH_CONF}" | tr -d '[:space:]' || true)

get_prop_encoded reports the on-disk bytes, so the trim happens in bash where the == "hstore" test already lives — which is the coupling you flagged, resolved by not needing a trimmed reader at all. backend : hstore with HG_SERVER_BACKEND unset now reaches the partition-wait check, and duplicate definitions resolve to the first one the way HugeConfig does instead of the last one grep found.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: The biggest cut available is to replace the awk YAML reader and the cross-side authenticator propagation with a refusal of any one-sided auth config, which keeps REST and Gremlin from splitting and drops roughly 250 lines across the entrypoint, props.awk and the test suite. Evidence: read the full diff at bf83032; ran the head get_prop_encoded and has_yaml_authentication_block helpers with the proposed check against neither-side, both-side (including auth\.authenticator : A with a quoted yaml scalar), REST-only, yaml-only and authenticator-less-block fixtures.

# 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() {

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 reader, and the propagation it feeds in align_auth_config, can go. Keep the guarantee (REST and Gremlin never end up on different authenticators) and drop the YAML parsing.

The PR already refuses one case, a yaml block with no readable authenticator, because carrying on would leave auth half-applied. Refuse every one-sided case the same way and the entrypoint no longer needs to know which class the yaml names, only whether each side names one. Both inputs already exist in this diff:

check_auth_sides() {
    local rest=0 yaml=0
    [[ -n "$(get_prop_encoded auth.authenticator "${REST_SERVER_CONF}")" ]] && rest=1
    has_yaml_authentication_block && yaml=1
    if (( rest != yaml )); then
        log "ERROR: authentication is configured in only one of" \
            "rest-server.properties and gremlin-server.yaml;" \
            "configure both or neither, then restart."
        return 1
    fi
}

Neither side: enable-auth.sh appends both, with its own AUTHENTICATOR_CLASS default. Both sides: its per-file guards make it a no-op. One side: stop.

That deletes get_yaml_authenticator (quotes, flow mappings, inline comments, indent scoping), get_prop and the get-decoded mode in props.awk, most of align_auth_config, and the test blocks for yaml scalars, sibling scope, escaped authenticators and the AUTHENTICATOR_CLASS default: roughly 250 lines. The trailing-space thread on props.awk line 279 goes away with get-decoded.

What you give up: an operator with a one-sided mounted config gets an error telling them to finish it, instead of the entrypoint finishing it for them, and two sides naming different classes are left alone without the WARN, as they were before this PR. Checked with the head helpers: neither and both (including auth\.authenticator : A next to a quoted yaml scalar) pass; REST-only, yaml-only and a block without an authenticator all refuse.

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.

Taken in b8801a6 — this is a better shape than what I had, and I have deleted the parser: get_yaml_authenticator, get_prop, the get-decoded mode in props.awk, the class comparison and the AUTHENTICATOR_CLASS export all go, and check_auth_sides decides by presence.

One change to your sketch, because the version as written reopens the fail-open this PR exists for. has_yaml_authentication_block is true whenever the mapping is present, so a mounted file with

authentication:
  authenticationHandler: org.apache.hugegraph.auth.WsAndHttpBasicAuthHandler

and auth.authenticator=com.example.MyAuth in the REST file gives rest=1, yaml=1 → passes. But snakeyaml resolves that mapping to no authenticator, Gremlin stays on AllowAllAuthenticator, and REST enforces — one side configured, silently. The nameless case is exactly the one your verification list expected to refuse, and it only refuses when the REST side is empty too.

So the read is three-valued (none / named / nameless) and a nameless mapping is refused on its own, before the comparison. It still never reads the class. A mapping that names nothing cannot be left to enable-auth.sh either, because its guard looks for the presence of authentication: and would then write only the REST file.

Two other things that came out of implementing this:

  • the key now has to be at column 0 (separate thread), so a nested authentication: under another feature reads as none rather than deciding the REST side;
  • the both sides, different classes case passes through untouched with no WARN, as you described. Left as is deliberately: with the per-file guards already in enable-auth.sh, that config is one the operator wrote on both sides, and guessing which of their two classes to propagate is the thing the parser existed to do badly.

AUTHENTICATOR_CLASS is no longer exported at all, so the operator-supplied value reaches enable-auth.sh untouched. That behaviour moved, so the test moved with it: two fresh trees, one with the variable set and one without, asserting which class lands in both files.

Suite is green apart from the four assertions this host cannot execute (MSYS text-mode stdio, chmod no-op, ln -s copying). Each of the four changes above was checked by reverting it on its own — backslash refusal, column 0, the nameless refusal, and the refusal path — and each turns the suite red by itself. Production delta is net -56 lines of shell and awk; I added net +105 lines of tests, so the overall diff is not the ~250-line reduction you estimated.

Takes the review's simplification: the entrypoint no longer parses which
authenticator gremlin-server.yaml names, so get_yaml_authenticator, the
get-decoded mode in props.awk and the class comparison in
align_auth_config all go away.  What is kept is the guarantee those served -
REST and Gremlin never end up with authentication on one side only - by
refusing every one-sided layout instead of completing it.

The refusal is kept honest by a three-state read of the yaml (none / named /
nameless).  Treating a mapping that names no authenticator as "no yaml side"
would pass a REST-only config straight through to enable-auth.sh, whose guard
only looks for the presence of the mapping and so would write the REST file
alone: REST on StandardAuthenticator, Gremlin on TinkerPop's
AllowAllAuthenticator default.  That is the fail-open this PR exists to close.

Also from the review:

- the authentication key must start at column 0, so a mapping nested under an
  unrelated feature no longer decides the REST side.  This drops the case the
  previous test asserted for an indented `authentication:`; a top-level key is
  what gremlin-server.yaml actually uses.
- props_set refuses a value whose encoded form ends in an odd number of
  backslashes.  Written where it is no longer the last line it swallows the
  line after it, and commons-configuration2 reads such a pair back as no
  property at all, so the entrypoint would publish a secret no server sees.
- the post-startup backend read goes through props.awk, so a mounted
  `backend : hstore` no longer skips the partition-wait check silently.

Net -56 lines of shell and awk, +105 of tests.  Every new case was checked by
reverting its fix: the backslash refusal, the column-0 requirement, the
nameless refusal and the refusal path itself each turn the suite red on their
own.  Suite green here apart from the four assertions this host cannot execute.
@Adarsh-Me

Copy link
Copy Markdown
Author

b8801a6 takes @bitflicker64's simplification and answers all five threads. Short version:

finding change what fails without it
props_set can write a value ending in an odd number of backslashes, which then swallows the next line refuse it; the file is left byte-for-byte untouched reverting the guard alone turns the suite red
authentication: matched at any indentation, so a nested mapping decided the REST side key must start at column 0; the indented-authentication: test is dropped and the nested case asserted instead same
get-decoded did not trim, so a trailing space produced a wrong WARN the mode is deleted — nothing compares the two classes any more n/a
the post-startup backend read accepted only = goes through props.awk and trims in bash, so backend : hstore reaches the partition-wait check n/a (behaviour asserted in the suite)
parse which authenticator the yaml names don't: check_auth_sides decides by presence and refuses one-sided layouts making the check always pass turns the suite red

Two things worth flagging rather than leaving in the diff:

The proposal as sketched reopened the fail-open. has_yaml_authentication_block is true for any mapping that exists, so a mounted file with an authentication: block that names no authenticator plus auth.authenticator set in the REST file reads as rest=1, yaml=1 → passes → snakeyaml resolves no authenticator → Gremlin on AllowAllAuthenticator, REST enforcing. The read is therefore three-valued (none / named / nameless) and nameless is refused by itself, before the comparison — still without ever reading the class.

Column 0 costs a test that used to pass. The old suite asserted authentication: opened the block; that and rejecting a nested mapping are the same bytes, so they cannot both hold. I dropped the permissive one: the shipped config puts the key at column 0 and TinkerPop resolves it as a top-level key, so the indented form was this script being more generous than what it models. Say so if you would rather keep the lenient reading and reject only the nested case by tracking the parent path.

Honest state of the run, same caveat as before: this host is Git Bash, so the four assertions that need a real CR byte, a working chmod, or a symlink cannot execute here — the suite passes with exactly those four neutralised in a scratch copy, and the committed file is unchanged. Production delta is net -56 lines of shell and awk; tests net +105, so the overall diff is not the ~250-line reduction the thread estimated.

CI on b8801a6 is action_required like every previous head on this fork PR, so nothing has executed it yet — could someone with write access approve the workflows, and take a look at the auth decision above?

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: The auth bootstrap still accepts mismatched REST and Gremlin authentication states and can mishandle mounted Java properties. Evidence: exact-head static review of docker-entrypoint.sh, enable-auth.sh, and props.awk; latest-head workflows await maintainer approval.

# A flow mapping keeps the authenticator on the same line as the
# key, so it has to count there too; missing it would report a
# configured mapping as nameless and refuse a valid deployment.
if (match($0, /authenticator[ \t]*:/)) { named = 1; exit }

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.

‼️ Blocking: yes. Summary: This match also counts YAML comment text, so authentication: {} # authenticator: com.example.Fake is reported as named while Gremlin has no authenticator. Evidence: yaml_auth_state finds the comment text and enable-auth.sh skips appending because the line starts with authentication:. Please ignore comments and scalar text and count only a real direct authenticator key.

}
# Any other column-0 key ends the mapping. A blank or whitespace-only
# line does not, because YAML does not close a mapping on an empty line.
inblk && /^[^ \t]/ { inblk = 0 }

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.

⚠️ Blocking: no. Summary: A column-zero YAML comment ends this mapping scan, so authentication: followed by # note and an indented authenticator is reported as nameless and blocks startup when PASSWORD is set. Evidence: this condition clears inblk on any first nonblank byte, including #. Please ignore comment lines when detecting the end of the mapping.

# Any other column-0 key ends the mapping. A blank or whitespace-only
# line does not, because YAML does not close a mapping on an empty line.
inblk && /^[^ \t]/ { inblk = 0 }
inblk && /^[ \t]+authenticator[ \t]*:/ { named = 1; exit }

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.

‼️ Blocking: yes. Summary: This accepts an authenticator at any depth, so authentication.config.authenticator counts even when the direct authentication.authenticator is absent and Gremlin uses AllowAllAuthenticator. Evidence: the exact-head flow marks the nested key named and lets REST/YAML parity pass; TinkerPop keeps config as a separate map. Please count only the direct child authenticator key.

set_prop "auth.admin_pa" "${PASSWORD}" "${REST_SERVER_CONF}"
# A refusal here exits the entrypoint under set -e, so enable-auth.sh can
# never run one-sided after it.
check_auth_sides

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.

‼️ Blocking: yes. Summary: The parity check only runs when PASSWORD is nonempty, so a mounted REST-only authenticator bypasses validation and the server can start with Gremlin unauthenticated. Evidence: check_auth_sides is called only inside this conditional and execution proceeds when PASSWORD is unset. Please validate mounted authentication state regardless of whether bootstrap creates an admin password.

# misses a mounted CRLF config and the factory is never wrapped for auth
# although both servers already believe authentication is on.
CR=$'\r'
if grep -Eq "^[[:blank:]]*gremlin[\\\\]?\\.graph[[:blank:]]*([:=]|[[:blank:]])[[:blank:]]*org\\.apache\\.hugegraph\\.HugeFactory[[:blank:]]*${CR}?$" "${CONF}/graphs/${GRAPH_CONF}"; then

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.

‼️ Blocking: yes. Summary: A legal Java-properties key written as gremlin\u002egraph is not matched here, so its HugeFactory value is left unwrapped when authentication is enabled. Evidence: Java Properties decodes the Unicode escape to gremlin.graph, but this grep/sed pair matches only literal or backslash-escaped dots. Please use the properties-aware helper or handle Unicode escapes before rewriting the graph factory.

if [[ -s "${file}" && -n "$(tail -c 1 "${file}")" ]]; then
printf '\n' >> "${file}"
fi
printf '%s\n' "$@" >> "${file}"

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.

‼️ Blocking: yes. Summary: If appending the YAML auth block fails while the other config files remain writable, this function returns nonzero but the script continues and can exit successfully after updating REST and graph settings. Evidence: enable-auth.sh has no errexit or append-status checks, while the entrypoint trusts its exit status. Please stop on every failed append and preserve a nonzero script result.

# logical entry, spanning exactly the physical lines it occupies.
function props_load(file, raw, rc, nl, stripped, next_raw, start, logical) {
NLINES = 0
while ((rc = (getline raw < file)) > 0) {

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.

⚠️ Blocking: yes. Summary: LF-only getline reads a bare-CR properties file as one record, so updating its first key can replace that record and drop every later key. Evidence: props_load strips only a final CR after getline, while java.util.Properties treats bare CR as a line ending. Please split LF, CRLF, and CR records before rewriting.

c = substr(s, i, 1)
if (esc) { esc = 0; continue }
if (c == "\\") { esc = 1; continue }
if (c == "=" || c == ":" || c == " " || c == "\t") { sep_at = i; break }

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.

⚠️ Blocking: no. Summary: Java properties treats form-feed as separator whitespace, but split_kv recognizes only equals, colon, space, and tab; auth.authenticator=... is therefore missed and a valid mounted configuration is refused. Evidence: the exact-head separator scan has no form-feed case. Please handle form-feed consistently in key scanning, leading whitespace, and separator trimming.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: yes. Summary: props.awk and the per-file guards fix the duplicate-definition cases they target, but enable-auth.sh and check_auth_sides still disagree about what counts as a configured side, so three mounted layouts pass the check and still end with only one of REST and Gremlin authenticating. Evidence: extracted get_prop_encoded, yaml_auth_state and check_auth_sides from docker-entrypoint.sh at b8801a6 and ran them, then the real enable-auth.sh, against temp conf trees (bash 5, gsed as sed); read HugeAuthenticator.loadAuthenticator and InitStore for how an empty auth.authenticator is treated. CI on this head is held at action_required, so the new shell suites have not run in CI.

-e '$a\ authenticationHandler: org.apache.hugegraph.auth.WsAndHttpBasicAuthHandler,' \
-e '$a\ config: {tokens: conf/rest-server.properties}' \
-e '$a\}' ${CONF}/${GREMLIN_SERVER_CONF}
if ! grep -Eq '^[[:blank:]]*authentication[[:blank:]]*:' "${CONF}/${GREMLIN_SERVER_CONF}"; 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.

Important: This guard accepts authentication: at any indentation, while yaml_auth_state in the entrypoint now only accepts column 0. A nested mapping therefore passes check_auth_sides as "neither side configured" and then stops this append, so only the REST side gets written.

Reproduced at b8801a6 with the layout from the new want_state none test:

host: 0.0.0.0
someFeature:
  authentication:
    authenticator: com.example.Nested

and a rest-server.properties without auth.authenticator. yaml_auth_state prints none, check_auth_sides returns 0, and enable-auth.sh appends auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator and auth.graph_store=hugegraph to rest-server.properties but leaves gremlin-server.yaml unchanged. REST then enforces StandardAuthenticator and Gremlin stays on TinkerPop's AllowAllAuthenticator. The reply on the column-0 thread says this case lets enable-auth.sh "write both from its own default"; it writes only one.

Please make this guard match the entrypoint: grep -Eq '^authentication[[:blank:]]*:'. Add a test that runs the real enable-auth.sh on the nested layout and asserts both files get an authenticator.

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.

Confirmed, and still live at 53a5edb — this one was not covered by the 16:10Z push. Reproduced against the real enable-auth.sh:

before: state=none  check_auth_sides=PASS | after: gremlin=none  rest=[...StandardAuthenticator] => ONE-SIDED

Guard anchored to column 0 in 2c9ebaf so the two files answer the same question:

if ! grep -Eq '^authentication[[:blank:]]*:' "${CONF}/${GREMLIN_SERVER_CONF}"; then

Test added at docker/test/test-docker-entrypoint.sh:1066, on your layout, running the real script: it first asserts check_auth_sides accepts the tree (a case the entrypoint refuses no longer tests anything), then requires yaml_auth_state to come back named, the REST class to read back through props.awk, exactly one auth.authenticator definition, and check_auth_sides to still hold afterwards. someFeature:'s own block is asserted byte-identical, so the fix cannot "resolve" this by rewriting the nested mapping.

Red before the change, green after: nested authentication mapping: gremlin-server.yaml is none, not named.

I did not check a CR-only or CRLF gremlin-server.yaml here — MSYS text mode hides CR bytes, so ^authentication against those shapes is CI's to confirm, not mine.

'}'
fi

if ! grep -Eq '^[[:blank:]]*auth[\\]?\.authenticator[[:blank:]]*([:=]|[[:blank:]])' "${CONF}/${REST_SERVER_CONF}"; 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.

Important: check_auth_sides treats an auth.authenticator with an empty value as "REST not configured", but this guard has no single meaning for that case. With auth.authenticator= it counts the key as present and skips the append. With a bare auth.authenticator line (no separator) it counts the key as absent and appends a second definition, which props.awk and HugeConfig ignore because the first definition wins. In both cases the yaml block is appended and REST keeps an empty authenticator.

Reproduced at b8801a6 with host: 0.0.0.0 in gremlin-server.yaml:

  • rest-server.properties has auth.authenticator=. The check passes, the yaml gets authenticator: org.apache.hugegraph.auth.StandardAuthenticator, and rest-server.properties keeps auth.authenticator=.
  • rest-server.properties has a bare auth.authenticator line. The check passes, and the file now holds auth.authenticator followed by auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator. get_prop_encoded auth.authenticator returns an empty string.

HugeAuthenticator.loadAuthenticator returns null for an empty class, so REST serves without authentication while Gremlin requires it, and auth.admin_pa has already been written from PASSWORD.

Please handle the defined-but-empty case one way on both sides. Either check_auth_sides refuses a definition with an empty value, or the REST write goes through set_prop (first definition, in place) instead of an append guarded by grep. Add a test for both spellings.

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.

Confirmed, still live at 53a5edb — also not covered by the 16:10Z push. All three spellings reproduce against the real script:

auth.authenticator=    -> gremlin=named  rest=[]  ONE-SIDED
auth.authenticator     -> gremlin=named  rest=[]  ONE-SIDED
auth.authenticator=    (trailing blanks) -> also rest=[]

To your framing of the two options, I took the second (write through set_prop) because the first would leave a hole: check_auth_sides already asks for the value (get_prop_encoded non-empty), so it counts an empty definition as unconfigured and lets the tree through — refusing it in the guard would fix the docker path only, while bin/enable-auth.sh from the tarball, which never runs the guard, would still write one side.

Worth adding to your measurement: Java also strips the trailing blanks, so auth.authenticator= parses to the empty string too. Compared against java.util.Properties directly (jdk 17), for auth.authenticator:

auth.authenticator=com.example.Foo -> PRESENT len=15
auth.authenticator=                -> PRESENT len=0
auth.authenticator                 -> PRESENT len=0   (bare, no separator)
auth.authenticator=   /  \t / ':' forms -> PRESENT len=0
(no key)                            -> ABSENT

props.awk's get agrees on every row, so the reader was already right and only the caller's question was wrong.

Fix in 2c9ebaf: both REST defaults go through props_set, which replaces the first definition where it stands and appends when there is none, so the empty placeholder is rewritten rather than followed by a definition first-definition-wins buries. props_has lost its last caller here and is removed; PROPS_MODE=has stays, since it answers a different question and is covered directly.

Tests at test-docker-entrypoint.sh:1076 loop all three empty spellings and assert the class reads back through props.awk, exactly one raw definition, the placeholder replaced on line 1 (not appended), unrelated lines kept — plus one case that auth.authenticator=com.example.OperatorAuth survives the default write, so the new props_set path cannot clobber an operator. Red before: empty definition [auth.authenticator=]: rest-server.properties reads back [].

# Any other column-0 key ends the mapping. A blank or whitespace-only
# line does not, because YAML does not close a mapping on an empty line.
inblk && /^[^ \t]/ { inblk = 0 }
inblk && /^[ \t]+authenticator[ \t]*:/ { named = 1; exit }

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.

Important: ^[ \t]+authenticator[ \t]*: matches at any depth inside the block, and the same-line match() on line 107 matches anywhere on the line. An authenticator that sits under config therefore counts as the Gremlin one.

Reproduced at b8801a6: both authentication: / authenticationHandler: ... / config: / authenticator: com.example.X and authentication: {config: {authenticator: X}} make yaml_auth_state print named. With a REST auth.authenticator present, check_auth_sides passes. TinkerPop reads authentication.authenticator only, so Gremlin falls back to AllowAllAuthenticator while REST authenticates.

Please count only a direct child. Record the indentation of the first key inside the block and accept authenticator: only at that indentation, and for the flow form only at the top level of the braces. Add both layouts to the want_state cases as nameless.

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.

Already fixed, and your test asks are in — but not by the push you were reviewing. 5afbb4a landed 16:10Z, 45 minutes after this review, and I had not replied on the thread, so nothing told you the code had moved. That is on me.

yaml_auth_state is now docker/yamlscan.awk, and both rules you asked for are there: the indentation of the first key inside the block is recorded and only that column counts (yamlscan.awk:248-249), and for the flow form keys are read at depth one only (yamlscan.awk:173), so {config: {authenticator: X}} is invisible to it. config: staying its own map is what the server sees.

Both layouts you named are want_state cases as nameless already: test-docker-entrypoint.sh:902 (block under config:) and :906 (flow inside a flow), with :911 keeping {config: {...}, authenticator: X} as named so the direct child does not get thrown out with the nested one.

The half of this that was not fixed was the mirror image in enable-auth.sh — its guard still accepted authentication: at any indentation, so a nested mapping reached the disagreement from the other side. Covered in 2c9ebaf on the column-0 thread.

Addresses the blocking review.  All eight findings reproduced first, and
every fix below was reverted to confirm its own test goes red.

props.awk, against java.util.Properties:

- Line terminators.  A bare CR ends a line in Java but not to getline, so a
  CR-only config reached the parser as one record: only its first key was
  ever seen, and rewriting that key replaced the whole record and dropped
  every later entry.  Measured before: a file of three properties, one of
  them auth.authenticator; after set graph=..., one property left.  Records
  are now split on \r\n, \n and \r, and the terminator each line arrived with
  is replayed so untouched lines keep their bytes.
- Form feed.  Java treats \f as whitespace either side of the separator, so
  `auth.authenticator<FF>=...` is that property; it was parsed into the key
  name instead, the guards read the file as unconfigured, and the append
  added a second competing definition.
- Two read modes the guards needed: PROPS_MODE=has, which answers "is this
  key defined" without confusing an empty definition with no definition, and
  PROPS_DECODED=1 for callers that compare a value.  Exit status 2 means an
  error and 1 means absent, so a caller wearing errexit cannot read an
  unreadable file as "not there" and append over it.

gremlin-server.yaml:

- yaml_auth_state moves to yamlscan.awk and answers about the mapping rather
  than the text.  It reported named for `authentication: {} # authenticator:
  X`, and for a class nested under config:, both of which pass the parity
  check while Gremlin runs on AllowAllAuthenticator; and it reported nameless
  for a valid mapping with a comment line inside it, refusing a deployment
  that should start.  Only a direct child counts, in block and flow form
  alike, comment text is not content, and an authenticator with no class is
  the nameless case.  A separate file, which is also what keeps it free of
  the apostrophe that breaks a shell-quoted awk program.
- check_auth_sides now runs on every start.  Inside the PASSWORD branch only,
  a mounted REST-side authenticator with no yaml mapping was never validated.
- enable-auth.sh wrapped the graph factory on a grep that matched a literal
  or backslash-escaped dot, so the legal `gremlin\u002egraph` spelling left
  the factory unwrapped with both servers told authentication was on.  That
  read/write now goes through props.awk, and the embedded-CR workaround the
  grep needed goes with it.  The script also stops on the first failed
  append: it exited 0 while a read-only mounted yaml left REST configured
  and the yaml not.

props.awk moves to the assembly bin/ it is packaged from, which is where
bin/enable-auth.sh finds it in the tarball as well as the image; the
Dockerfile COPY of it is gone, since the image takes bin/ from the assembly.

Verified: props.awk against java.util.Properties (javac/java 17) over a
corpus of terminator, separator and escape forms, on read and on rewrite, 0
disagreements; yamlscan.awk over 21 shapes; both entrypoint suites; and the
eight findings as a table, 8/8 failing at b8801a6 and 8/8 passing now.  Not
run here: mawk (no Linux container on this host), snakeyaml, and the CR-byte,
chmod-mode and symlink assertions, which need a host where those primitives
behave; they are gated to say so rather than pass quietly, and they run in CI.
Master added HG_SERVER_STARTUP_TIMEOUT_S validation and the runtime-layer
rework from apache#3194, both in the files this branch rewrites.

# Conflicts:
#	hugegraph-server/hugegraph-dist/docker/docker-entrypoint-test.sh
@Adarsh-Me

Copy link
Copy Markdown
Author

All eight reproduced before touching anything, and every fix was reverted to confirm its own test goes red. Pushed in 5afbb4a; master merged in 53a5edb (mergeable: true again -- #3194 and the startup-timeout change had landed in the same files).

You were right on all eight. Table of what each one was, measured against java.util.Properties where the question was properties grammar:

thread what it was at b8801a6 now
r4083528783 authentication: {} # authenticator: X counted as named because the scanner matched the comment text named nameless
r4083528796 a column-zero comment line ended the mapping, refusing a valid deployment nameless named
r4083528809 authenticator accepted at any depth, so authentication.config.authenticator counted named nameless
r4083528814 check_auth_sides only ran inside the PASSWORD branch exit 0, server starts exit 1, refused before init-store
r4083528824 gremlin\u002egraph matched no grep pattern, factory left unwrapped org.apache.hugegraph.HugeFactory HugeFactoryAuthProxy
r4083528831 a failed yaml append on a writable REST file still exited 0 exit 0, half-configured exit 1
r4083528843 bare-CR file read as one record; rewriting its first key dropped every later one auth.authenticator gone read and survives
r4083528855 form feed not treated as separator whitespace key invisible parsed as Java sees it

The first, second and third were the same root cause and the worst of them: two of the three produced a false named, which is a passed parity check with Gremlin on AllowAllAuthenticator.

properties/yaml readers

  • props.awk now splits records on \r\n, \n and \r and remembers each line's terminator, so a rewrite replays untouched bytes. Your r4083528843 description was exactly right, and the rewrite is worse than "drop every later key" in one respect worth naming: auth.authenticator is usually the later key, so the surviving file is the unauthenticated one.
  • \f is now separator whitespace on both sides of the separator, in the leading-whitespace strip and in the comment/blank-line test.
  • The yaml reader moved out of the shell into yamlscan.awk. It answers about the mapping, not the text: column-zero mapping only, direct child only, in block and flow form alike (a flow key counts at depth one, so {config: {authenticator: X}} does not), comments stripped outside quotes, and a direct authenticator with an empty/null/~ value treated as nameless rather than named. The last one is not in your list -- it is the same fail-open shape as r4083528783, so I closed it while I was in there.
  • enable-auth.sh reads and writes through props.awk instead of grep/sed. The embedded-CR workaround the old pattern needed goes with it, since the reader now handles terminators.

One packaging change to look at: props.awk moved from docker/ to src/assembly/static/bin/, because bin/enable-auth.sh now needs it and the tarball does not ship docker/. That directory's assembly fileSet (<include>*</include>) picks it up with no descriptor change, and the image gets it from the same place it gets every other bin/ file, so the COPY .../props.awk . line added earlier in this PR is gone. yamlscan.awk stays in docker/ with an explicit COPY -- only the entrypoint uses it.

Two smaller changes the review pushed me into: PROPS_MODE=has, which answers "is this key defined" without confusing auth.authenticator= (present, empty) with absent -- appending a default on top of an empty first definition would have left the empty one in force; and exit status 2 now means an error while 1 means absent, so a caller wearing errexit cannot read an unreadable file as "not there".

Verification

  • props.awk against real java.util.Properties (javac/java 17) over 15 file shapes -- LF, CRLF, CR-only, mixed, no-final-newline, \f on either side of the separator, \u002e and \. in keys, continuations, indentation, :/whitespace separators. Two assertions per shape: every property Java sees must be visible to the reader with the same value, and rewriting one key must leave every other key intact and the key set unchanged. 21 failing assertions at b8801a6, 0 now.
  • yamlscan.awk over 21 yaml shapes (the three findings, the positive forms a wrong reader must keep accepting, and the negatives).
  • The eight findings as a standalone before/after table: 8/8 fail at b8801a6, 8/8 pass now.
  • test-docker-entrypoint.sh and docker-entrypoint-test.sh both green on the merged tree, which includes the startup-timeout cases from fix(docker): make the Server startup timeout configurable #3187.

What did not run here, so please read the above with this attached: no Linux container on this host, so mawk never executed -- the image's /usr/bin/awk is mawk on jammy, and I kept the new awk to substr/index/match, no RT, no regex RS, no gensub, and gawk --posix --lint is clean apart from the parameter-shadowing warnings this file already had. The Java oracle is 17, not the 11 the image runs; the grammar cases here are identical between them, but that is reasoning, not a measurement. snakeyaml was never run either, so the yaml rules are from the spec plus a written trace, not from the parser that will actually read these files. Three assertions in the unit suite -- CRLF byte preservation, config-mode preservation, symlinked config -- cannot be evaluated on a Windows host at all (MSYS gawk drops the CR of a CRLF pair on read, chmod does not affect what stat reports, ln -s copies), so they are gated behind host probes that print a skip line rather than pass quietly; they will genuinely run in CI.

CI at 53a5edb: six workflows registered, all completed / action_required -- so none of them have run, and nothing here is green. A maintainer with write access still needs to approve the pending deployments; mergeable_state is blocked on that, not on anything in the diff.

check_auth_sides and enable-auth.sh decide one question from two files, and
the guards here asked it about presence while the entrypoint asks it about the
value.  Both findings were reproduced against this script before anything
changed, and both new test groups were run against the unpatched script to
confirm they go red.

- Column-0 `authentication` only.  The yaml guard accepted the key at any
  indentation while yamlscan.awk counts only a column-0 mapping, so a config
  that nests `authentication` under another feature read as `none`: parity
  held, the entrypoint ran this script, and the guard then saw the nested key,
  skipped the append and wrote the REST side alone.  Gremlin stayed on
  TinkerPop's AllowAllAuthenticator under a StandardAuthenticator REST.  The
  nested block is still left byte-identical, and that is asserted too.
- A defined-but-empty authenticator is the unconfigured side.  props_has
  reports `auth.authenticator=`, a bare `auth.authenticator` line and
  `auth.authenticator=   ` as present, and they are: measured against
  java.util.Properties all three parse to the empty string, and
  HugeAuthenticator.loadAuthenticator returns null for that, so REST serves
  without authentication.  The presence guard skipped the append on exactly
  the side that needed it.  The two REST defaults now go through props_set,
  which rewrites an existing definition where it stands -- the placeholder is
  replaced rather than followed by a second definition that
  first-definition-wins would bury -- and appends when there is nothing to
  replace.  An operator-written class is still never overwritten.
- props_has had no caller left once both defaults moved, so it is gone.
  PROPS_MODE=has stays in props.awk: it is a documented answer to a different
  question and is still covered directly.

Not verified here: CR-only and CRLF gremlin-server.yaml, which MSYS text mode
cannot observe, and mawk, which is not installed on this host.  Both are
exercised by CI on Ubuntu.

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: The auth parity check still accepts YAML null nodes as authenticated, and valid mounted forms can be rejected or rewritten inconsistently. Evidence: exact-head static review of yamlscan.awk, enable-auth.sh, and docker-entrypoint.sh; latest-head workflows are action_required.

# An authenticator entry only counts when it actually names a class.
function names_class(v) {
v = trim(v)
return v != "" && v != "null" && v != "~"

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.

‼️ names_class() treats only the lowercase text null and ~ as empty. SnakeYAML accepts !!null null (and case variants such as NULL) as a null node, so this scanner reports it as named and check_auth_sides() can start REST authentication while Gremlin has no authenticator. Please recognize YAML null tags/case variants or fail closed on unsupported tags, and cover this in the parity tests.

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 d5ccb94. names_class() now refuses null in any case, plus !!null and an empty quoted scalar; a value whose type comes from an unresolvable tag fails closed instead of guessing. A quoted "null" deliberately stays named, since quoting makes it a string. Verified by reverting: NULL reported named before, nameless after. Cases added to the parity group.

for (i = 1; i <= n; i++) {
c = substr(s, i, 1)
if (q != "") {
if (FST == "key") CUR = CUR c

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.

⚠️ In a quoted flow value, this branch never appends characters while FST == "val"; CUR_VAL stays empty and {authenticator: "org.example.Auth"} is classified as nameless. A valid mounted configuration with matching REST authentication is therefore refused before server startup. Preserve quoted value contents with escape handling and add a quoted flow authenticator case.

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 d5ccb94. Quoted bytes now accumulate for flow values as they already did for keys, with the double-quoted backslash escape honoured, so {authenticator: "org.example.Auth"} reads named. Reproduced first: it returned nameless. Reverted to confirm the new quoted-flow cases go red; single-quoted, sibling and nested-under-config forms are all covered.

# called this script, but this guard saw the nested key and skipped the append,
# writing the REST side only -- StandardAuthenticator on REST, TinkerPop's
# AllowAllAuthenticator on Gremlin.
if ! grep -Eq '^authentication[[:blank:]]*:' "${CONF}/${GREMLIN_SERVER_CONF}"; then

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.

⚠️ yamlscan.awk accepts and unquotes a quoted top-level key such as "authentication":, but this guard matches only the bare spelling. With PASSWORD set, an existing valid block is missed and a second block is appended; YAML can reject the duplicate or select the appended default while REST retains its existing authenticator. Reuse the same YAML-aware check here and cover quoted top-level keys.

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 d5ccb94. The guard now asks yamlscan.awk the same question check_auth_sides asks, so anything other than none leaves the operator block alone. The release tarball ships no yamlscan.awk, and there the fallback grep learned the quoted spellings too. Measured before: one top-level key in, two out; now one. Covered in both layouts.

# separator is seen at all, and first-definition-wins matches HugeConfig; the
# grep this replaces only ever accepted `=`. Trailing whitespace is dropped
# here rather than in the reader, which reports the on-disk bytes verbatim.
ACTUAL_BACKEND=$(get_prop_encoded "backend" "${GRAPH_CONF}" | tr -d '[:space:]' || true)

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.

⚠️ get_prop_encoded() returns the on-disk escaped value; props.awk decodes values only when PROPS_DECODED=1. For a valid properties entry backend=h\u0073tore, the JVM reads hstore but this comparison skips wait-partition.sh, allowing startup to continue before HStore partitions are assigned. Read this key in decoded mode before comparing it.

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 d5ccb94 through a new get_prop_decoded(). Measured against java.util.Properties: the escaped spelling is hstore to the JVM, so wait-partition.sh is now reached, and the rocksdb case still skips it - asserted end to end in docker-entrypoint-test.sh. The REST presence check was measured as well and does not need decoding, so it stays encoded.

Four findings from the re-review.  Each was reproduced against this tree
before anything changed, and each fix was then reverted to confirm its own
test goes red -- 20 cases red before, 20 green after.

yamlscan.awk

- names_class() accepted any spelling of null except the lowercase one.
  snakeyaml resolves null case-insensitively, so `authenticator: NULL`,
  `Null` and `nUll` are the null node, and `!!null` states it outright; every
  one of them reported `named`, which is the one answer that cannot be
  forgiven: check_auth_sides then saw authentication on both sides and let
  REST enforce StandardAuthenticator over a Gremlin on AllowAllAuthenticator.
  An empty quoted scalar belongs here too -- the JVM hands loadAuthenticator
  the empty string and it returns null -- as does a value whose type comes
  from a tag this scanner cannot resolve, which is now refused rather than
  guessed at.  A quoted "null" deliberately stays `named`: quoting makes it a
  string, and a class that does not exist fails loudly at startup instead of
  silently opening the server.  Pinned as a case so a later tightening of the
  null rules has to move it on purpose.
- scan_flow() lost the contents of a quoted value.  Only keys accumulated
  inside the quotes, so {authenticator: "org.example.Auth"} arrived at
  commit_val() empty and read as nameless, refusing a valid mounted config
  before the server ever started.  Quoted bytes now accumulate for a value as
  they do for a key, with the double-quoted backslash escape honoured so an
  inner quote does not end the scalar early.

enable-auth.sh

- The yaml half of the append guard now asks the same reader check_auth_sides
  uses.  grep only ever matched the bare spelling, so an operator's
  `"authentication":` block read as absent and a second default block was
  appended beside it, after which the two servers resolve the key in opposite
  directions while REST keeps the authenticator the operator named.  Measured
  before the change: one top-level key in, two out, in both the image and the
  tarball layout.  The image gets yamlscan.awk from the install home, so
  PROPS_AWK-style discovery covers it; the plain release tarball carries no
  copy, and there the fallback grep at least knows the quoted spellings.
  Anything other than `none` means the mapping is there and is not this
  script's to duplicate.

docker-entrypoint.sh

- ACTUAL_BACKEND is compared against a literal, so it has to be read decoded.
  Against java.util.Properties as the oracle, a mounted
  `backend=h<escape>s</escape>tore` is hstore to the JVM while the on-disk
  bytes are not, and the comparison then skipped wait-partition.sh and let
  startup continue before partitions were assigned.  get_prop_decoded() is
  the new reader and the end-to-end case asserts wait-partition.sh is reached
  for the escaped spelling and still not reached for rocksdb.  The other
  get_prop_encoded() callers were measured rather than swept along: the
  presence test at the parity check does not need decoding, because the
  encoded reader already trims trailing blanks the way Properties does, so
  the two readers agree there and only this literal comparison was wrong.

Verified: yamlscan.awk over the null spellings, quoted flow values and nested
flow cases (10 red / 20 ok after); enable-auth.sh in both layouts; the
escaped-backend read against java.util.Properties on jdk17; both CI-wired
shell suites at exit 0 with output identical to the pre-change baseline, and
docker-entrypoint-test.sh reaching its PASS line; the wait-partition case
shown red by restoring get_prop_encoded at that one call site.

Not verified here: mawk, which is not installed on this host; CR-only and
CRLF gremlin-server.yaml, which MSYS text mode cannot observe; snakeyaml
itself, never executed, so the null spellings follow the resolver's documented
behaviour rather than a run of it, and the refusal of an unresolvable tag is a
judgement call about the safe direction rather than a measurement; and no
Docker daemon, so no image build.

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: Several valid or accepted configuration forms can bypass REST/Gremlin authenticator parity; the properties writer also follows predictable staging symlinks. Evidence: Exact-head static control-flow review of yamlscan.awk, props.awk, and HugeConfig.loadConfigFile().

}
if (v == "~") return 0
if (tolower(v) == "null") return 0
return 1

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.

‼️ names_class() treats YAML anchor/alias syntax as a non-empty class name. For valid YAML with authentication: {authenticator: &noAuth null}, SnakeYAML resolves the value to null but the scanner reports named; with REST authentication configured, check_auth_sides() accepts the mismatch and Gremlin can remain unauthenticated. Resolve aliases with the runtime YAML parser or reject anchors/aliases here, and cover null/empty aliases.

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.

Confirmed: authenticator: &noAuth null answered named. Fixed in cc9cc8c - the anchor label comes off and the text behind it decides, an anchor with nothing behind it is empty, and an alias is refused rather than guessed. &cls com.example.Anchored still names its class. Reproduced against d5ccb94 first; the new group is red without the fix.

if (!split_pair(line)) next
if (child < 0) child = indent_of(line)
if (indent_of(line) != child) next
if (names_authenticator(K_TXT) && names_class(V_TXT)) finish("named")

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.

‼️ A later duplicate direct child can replace the value this scanner has already accepted. In block YAML, authenticator: org.apache.hugegraph.auth.StandardAuthenticator followed by authenticator: null hits finish("named") on the first row even though SnakeYAML keeps the last value; the parity check can pass while Gremlin has no authenticator. Scan the full mapping with the server's last-wins behavior or reject duplicate keys, and cover block and flow forms.

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.

Reproduced in both forms, fixed in cc9cc8c: block and flow mappings are read to their end, and a direct authenticator defined twice is refused - reason on stderr - instead of being settled by the first row seen. snakeyaml keeps the last value or rejects the document, so first-match was never the server's answer. Group red without the fix.

}
if (is_quote(c)) { q = c; continue }
if (c != ":") continue
if (i == n || substr(s, i + 1, 1) ~ /^[ \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.

⚠️ split_pair() only recognizes a colon followed by end of line, space, or tab. On CRLF input, the top-level line is authentication:\r, so this check misses it; its indented children are skipped and yamlscan.awk reports none. With Gremlin authentication present and REST authentication absent, check_auth_sides() accepts the config and leaves REST open. Strip the carriage return before parsing and add a CRLF parity case.

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.

Confirmed: authentication:\r matched no key, so a CRLF gremlin-server.yaml read as none and check_auth_sides accepted REST-open beside an authenticating Gremlin. Fixed in cc9cc8c - trailing CR is line noise, taken off before a line is split. The fixture writes one CR for the host (MSYS text mode needs two) and skips itself when it cannot.

# logical entry, spanning exactly the physical lines it occupies.
function props_load(file, raw, rc, content, nl, stripped, next_raw, start, logical) {
content = ""
while ((rc = (getline raw < file)) > 0)

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.

‼️ props_load() reads only PROPS_FILE, while HugeConfig loads properties through Commons Configuration with includes enabled by default. If the main file has include=rest-auth.properties and the included file defines auth.authenticator, this helper reports REST authentication as absent; with no YAML auth mapping, the entrypoint passes the parity check and starts a REST-protected/Gremlin-open server. Resolve includes consistently or fail closed when an include is present.

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 cc9cc8c by failing closed: a file carrying a live include is refused in get, has and set (exit 2), nothing is written, and check_auth_sides says the REST side could not be read instead of reporting a one-sided config. included.foo and a commented #include= still read. Resolving includes needs the parser HugeConfig uses, which I could not run here.

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 5940fa5. props.awk now refuses both include and includeOptional, case-insensitively, in every mode (exit 2), so INCLUDE= or IncludeOptional= no longer read as ordinary properties. Controls kept: included.filter and a commented #include still read. Extended the test to every spelling; it fails without the fix. Did not run commons-configuration2 here.

# its mode, and the chmod after close repairs a stale tmp left behind
# by a crashed run.
tmp = file ".tmp"
system("umask 077 && : > " shquote(tmp))

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.

⚠️ This predictable .tmp path is opened with truncating redirection without checking for a symlink; the later AWK redirection follows it too. If a writable mounted config directory contains rest-server.properties.tmp as a symlink, the default-root Docker entrypoint can overwrite a file elsewhere in the container while writing credentials. Use an exclusive 0600 temporary file with no-follow semantics, and secure the .bak destination the same way.

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 cc9cc8c: both staging names come from an exclusive mktemp create (0600 whatever the umask), so there is no predictable path to arrange and nothing to follow. The rollback group follows the paths props.awk reports instead of guessing names, and a planted <file>.tmp/.bak is asserted untouched. The symlink case is host-gated and did not run here.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: yes. Summary: props.awk reads properties with the java.util.Properties grammar, but HugeConfig loads rest-server.properties and hugegraph.properties through commons-configuration2 2.8.0, which trims each line and keeps \ escapes. At d5ccb94 that difference means a mounted gremlin.graph=...HugeFactory with a trailing blank is no longer wrapped in HugeFactoryAuthProxy (master wraps it), and a PASSWORD ending in a space leaves REST with no authenticator while check_auth_sides passes. Evidence: ran head enable-auth.sh, props.awk and the entrypoint helpers (encode_prop_value, set_prop, yaml_auth_state, check_auth_sides) against copies of the shipped conf/ tree, and read every result back with commons-configuration2 2.8.0 PropertiesConfiguration and java.util.Properties (JDK 17). Base enable-auth.sh at 83ef9f3 (the PR base) was run the same way with GNU sed for the gremlin.graph comparison. Not covered: images were not built and mawk was not exercised (no Docker daemon here), and CI has not run at this head: all six workflow runs are action_required.

# Wrap the graph factory only when it really is the plain HugeFactory, which is
# a question about the decoded value, so it goes through the same reader.
GRAPH_FACTORY=$(props_get "gremlin.graph" "${CONF}/graphs/${GRAPH_CONF}")
if [[ "${GRAPH_FACTORY}" == "org.apache.hugegraph.HugeFactory" ]]; 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.

Important: This compares the decoded value without trimming it, so a gremlin.graph line with trailing whitespace is no longer wrapped, and the server still opens the graph with the plain HugeFactory. commons-configuration2 2.8.0 (what HugeConfig loads with) trims the line. props.awk keeps the trailing blank, which is java.util.Properties behaviour.

I copied the shipped conf/ tree, added one trailing space to gremlin.graph=org.apache.hugegraph.HugeFactory, and ran enable-auth.sh:

base (sed s/gremlin.graph=...HugeFactory/.../g)  -> gremlin.graph=org.apache.hugegraph.auth.HugeFactoryAuthProxy 
                                                   cc2 getProperty -> [org.apache.hugegraph.auth.HugeFactoryAuthProxy]
head d5ccb94 (props_get + exact ==)              -> gremlin.graph=org.apache.hugegraph.HugeFactory 
                                                   cc2 getProperty -> [org.apache.hugegraph.HugeFactory]

enable-auth.sh exits 0 in both runs. At this head, authentication is enabled on both sides but the graph is not behind HugeGraphAuthProxy. GraphManager only logs a warning about that (GraphManager.java:1851). So a mounted config that master wrapped now runs without per-user access control on the graph. The same trim gap was raised for the old get-decoded path in an earlier round, and it came back when this comparison moved to PROPS_DECODED=1.

Requested change: strip trailing space, tab and form feed from GRAPH_FACTORY before the comparison, or make the decoded read trim the way commons-configuration2 does. Add a test case with a trailing blank after HugeFactory that asserts the factory is wrapped.

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.

Confirmed and fixed in cc9cc8c, at the comparison site rather than in the decoded read: trailing blanks come off before gremlin.graph is compared, so a mounted ...HugeFactory is wrapped again while a factory this script does not own is left alone. The test runs enable-auth.sh over both the raw and \ spellings, red without the fix.

nbs = 0
while (nbs < length(enc_val) && substr(enc_val, length(enc_val) - nbs, 1) == "\\")
nbs++
if (nbs % 2 == 1)

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.

Important: This guard exists so that a written value never turns the next line into a continuation under commons-configuration2. It misses \ at the end of a value, which is how encode_prop_value writes a trailing space. cc2 trims the line before its continuation check, so abc\ becomes abc\ and swallows the following line.

I ran the entrypoint order (check_auth_sides, set_prop auth.admin_pa "abc ", then enable-auth.sh) on the shipped default conf/. The run ends like this:

rest-server.properties tail:
  auth.admin_pa=abc\ 
  auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator
  auth.graph_store=hugegraph
check_auth_sides                -> passes (props.awk: rest named, yaml named)
cc2 getProperty auth.authenticator -> null
cc2 getProperty auth.admin_pa      -> [abcauth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator]

The REST server then loads with no authenticator while Gremlin requires one. That is the one-sided state check_auth_sides exists to refuse. The same thing happens to whatever line follows a auth.token_secret that ends in a space. The encoder is older than this PR, so master behaves the same way. But this PR adds the parity check and this cc2 guard for exactly this hazard, and neither catches it. cc2 also keeps \ inside a value: auth.admin_pa=Strong\\Pass\ 9!, the fixture in docker-entrypoint-test.sh, reads back as Strong\Pass\ 9! rather than Strong\Pass 9!.

Requested change: have encode_prop_value write a space as \u0020. Both readers decode that the same way: cc2 and java.util.Properties both return abc for abc\u0020. Also extend this guard to refuse an encoded value that ends in a backslash followed by whitespace. Add a round-trip test that checks the line after a trailing-space secret is still a separate property.

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.

Done as suggested in cc9cc8c: encode_prop_value writes a space as \u0020, and the guard judges the right-trimmed form, refusing abc\ and abc\\\ while still accepting abc\\ . Round trip added - auth.admin_pa=abc\u0020 on disk, java.util.Properties on JDK 17 returns abc with the next property intact. commons-configuration2 was not run here; no jar on this host.

system("chmod 600 -- " shquote(tmp))
bak = file ".bak"
cmd = "umask 077 && cp -- " shquote(file) " " shquote(bak)
if (system(cmd " && chmod 600 -- " shquote(bak)) != 0)

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.

Minor: chmod 600 -- file fails with BSD chmod, which reads -- as a file name after the mode. This file now ships in the release tarball's bin/, so bin/enable-auth.sh fails on macOS. On macOS (/bin/chmod), enable-auth.sh against the shipped conf/ printed chmod: --: No such file or directory and props.awk: cannot back up .../rest-server.properties before the copy-back, then exited 1. By then it had already appended the authentication block to gremlin-server.yaml and left rest-server.properties.tmp and .bak in conf/. The master script also failed on macOS (BSD sed rejects $a\), but it wrote nothing. Requested change: drop the -- from the two chmod calls here and on line 312. The paths are already quoted by shquote and never start with - in these calls.

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 cc9cc8c - and both chmod calls are gone, not just the --: the staged files are created 0600 by an exclusive mktemp now, so no mode is left to repair. Nothing passes -- to chmod. A config whose name starts with - is asserted to still write. BSD chmod itself could not be run on this host.

Eight findings from the re-review, all reproduced against this tree before
anything was edited, and each fix then reverted to confirm its own test group
goes red -- eight red groups against the previous commit, eight green with this
one, and both CI-wired suites (docker/test/test-docker-entrypoint.sh,
docker/docker-entrypoint-test.sh) exit 0 with output unchanged from baseline.

yamlscan.awk

- names_class() read an anchor label as the value.  `authenticator: &noAuth
  null` is a valid document whose authenticator resolves to null, and an alias
  `*noAuth` names a node this scanner does not resolve; both answered `named`,
  which is the direction that lets REST enforce StandardAuthenticator over a
  Gremlin left on AllowAllAuthenticator.  The label now comes off and the text
  behind it decides, an anchor with nothing behind it is empty, and an alias is
  refused.  `&cls com.example.Anchored` still names its class, because
  refusing a mounted config before startup is its own bug.
- A mapping was answered at the first direct `authenticator` it met.  The
  server reads the whole node, and snakeyaml either keeps the last value for a
  repeated key or rejects the document, so `authenticator: com.example.First`
  followed by `authenticator: null` reported `named` for a Gremlin that ends up
  with no authenticator.  Block and flow mappings are now read to their end, a
  single entry is judged on its own value, and a repeated direct child is
  refused with the line to fix on stderr rather than resolved by whichever row
  the scan happened to reach first.
- A CRLF gremlin-server.yaml -- which is what a config edited on Windows and
  mounted into the image is -- ended its lines with CR bytes the scanner kept.
  `authentication:\r` matched no key, so the whole mapping read as `none`: with
  nothing on the REST side either, check_auth_sides saw two sides agreeing and
  started a server that authenticates on Gremlin and leaves REST open.  Trailing
  CR is line noise now, taken off before a line is split.

props.awk

- props_load() read one file while HugeConfig reads a file plus whatever its
  `include` directive splices into it, so `auth.authenticator` defined in the
  included file was reported absent from this one, and the spliced order also
  decides which of two definitions wins.  Neither question is answerable from
  the bytes here, so a file that carries a live `include` is refused in get,
  has and set (exit 2), nothing is written, and check_auth_sides says plainly
  that the REST side could not be read instead of sending the operator to the
  parity message.  `included.foo` and a commented `#include=` still read.
- The staged rewrite used `<file>.tmp` and `<file>.bak`: predictable names,
  opened through a truncating redirection that checks nothing, so a mounted conf
  directory holding either name as a symlink had the default-root entrypoint
  write credentials through it into another file.  Both are exclusively created
  unpredictable names now (mktemp, 0600 whatever the umask), which is also what
  removed the two `chmod 600 -- ` calls that BSD chmod reads as a file name.
- The odd-backslash guard looked at the encoded value, and `abc\ ` ends in a
  space, so it passed a value that commons-configuration right-trims into
  `abc\`, which then swallows the line under it.  The guard judges the trimmed
  form; docker-entrypoint.sh writes a space as \u0020, which both readers decode
  to the same space and the trimmer has nothing to take.  A round trip through
  java.util.Properties on JDK 17 gives `abc ` back with the following property
  intact.
- enable-auth.sh compared gremlin.graph without trimming, so a mounted factory
  with a trailing blank was left outside HugeFactoryAuthProxy in a tree both
  servers believed was authenticated; GraphManager only warns about that.  It
  compares the spelling the server resolves now, and a factory it does not own
  is still left alone.

Not verified here, named rather than assumed: commons-configuration2 and
snakeyaml were not executed -- no jars, no local repository, and no Maven
resolve on this host -- so the trim and null-resolution behaviours come from the
reviewers' measurements plus the documented grammar; mawk is absent, so the awk
changes ran under gawk only; the symlinked temp-file case is gated on a host
that can make symlinks and did not run here; there is no Docker daemon, so the
image was not built.

@imbajin imbajin left a comment

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.

Review at cc9cc8c

Blocking: yes. Score: 6.5/10. Five independent review lanes completed, followed by independent reproduction of the parser findings.

  • The existing include finding remains partially unresolved. Commons Configuration 2.8.0 also accepts includeOptional and case-insensitive forms of both directives. props.awk rejects only exact lowercase include. A real-parser fixture loads a REST authenticator from the included file while check_auth_sides returns success with REST absent and YAML none. Please reject all supported include directive spellings, or resolve them with the actual parser.
  • The inline findings cover effective YAML authentication detection and a reproducible CI environment leak.

Evidence used real Commons Configuration 2.8.0 and TinkerPop 3.5.1 Settings.read() with SnakeYAML 1.27. Shell checks passed in isolated copies with macOS limitations; this is not a complete Linux image validation. The current rocksdb CI job fails at the escaped-backend assertion, not an unrelated Java test.

if (trim(line) == "") next
# A column-0 line after the comment was stripped is a sibling key, so the
# mapping has ended and what was recorded while reading it is the answer.
if (indent_of(line) == 0) finish(auth_state())

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.

‼️ [P1] Fail closed when the effective root authentication mapping is not identified. With two top-level authentication mappings, this early return accepts the first named authenticator, while TinkerPop Settings.read() uses the final empty mapping and defaults to AllowAllAuthenticator. An indented root mapping is also valid to Settings.read() but is skipped at line 314 and reported as none, allowing the opposite REST/Gremlin mismatch. Both cases were reproduced with TinkerPop 3.5.1 / SnakeYAML 1.27. Detect duplicate root mappings through EOF and recognize or explicitly reject unsupported root layouts; test against the runtime parser.

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 5940fa5. The scanner now reads to EOF and refuses a duplicate root authentication mapping (nameless, count on stderr), and anchors on the first key's indentation, so an indented root mapping resolves instead of reporting none. A nested authentication still reports none. Tests added; each fails without the fix, confirmed by reverting. Did not run SnakeYAML here.

fi
(
cd "${TEST_HOME}"
bash ./docker-entrypoint.sh

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.

⚠️ [P2] Isolate this config-driven invocation from the CI backend environment. Server CI exports BACKEND=rocksdb; the entrypoint maps it to HG_SERVER_BACKEND and overwrites this fixture's escaped hstore, so the assertion at line 436 fails. This reproduces with BACKEND=rocksdb and passes when BACKEND and HG_SERVER_BACKEND are unset. Clear inherited backend variables for these config-driven cases rather than changing production environment precedence.

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 5940fa5. The harness now unsets inherited BACKEND/HG_SERVER_BACKEND/PD_PEERS/HG_SERVER_PD_PEERS at the top, so the escaped hstore fixture decides the stabilization check instead of the CI matrix value. Production env precedence is unchanged; env-driven cases set it per invocation. Reproduced: the test failed under BACKEND=rocksdb before, passes after.

…cally

Three findings from imbajin's re-review at cc9cc8c. Each was reproduced
against the previous commit first, then the test was reverted to confirm it
goes red without the fix; both CI-wired suites exit 0 after the fix.

props.awk -- commons configuration 2 splices another file on BOTH `include`
and `includeOptional`, and matches the directive name case-insensitively, so
rejecting only the exact lowercase `include` let `INCLUDE=`, `IncludeOptional=`
and friends read and rewrite as ordinary properties: auth.authenticator defined
in the spliced file stayed invisible, which is the REST-open/Gremlin-protected
boot the guard exists to stop. Every spelling a real loader honours is now
refused in all modes (exit 2), with `included.filter` and a commented `#include`
kept as controls that still read.

yamlscan.awk -- two unsafe answers, both from trusting column 0 and the first
mapping:
  - two top-level `authentication` mappings resolved through the FIRST one, so a
    config the empty second mapping leaves on AllowAllAuthenticator reported
    `named`.  The scanner now reads to EOF and refuses a duplicate root mapping
    (nameless, with the count on stderr) the same way it already refuses a
    duplicate direct authenticator.
  - a root mapping written indented below a document marker -- valid to
    Settings.read() -- was skipped and reported `none`, the opposite mismatch.
    "column 0" became "the indentation of the document root", learned from the
    first key:an-indented `authentication:` nested under another real key is
    still not the server's, and still reports `none`.

docker-entrypoint-test.sh -- server-ci.yml exports BACKEND=rocksdb for the only
matrix leg that runs these tests, the entrypoint maps it to HG_SERVER_BACKEND,
and that overwrote the fixture's escaped `h\u0073tore`, so the stabilization
assertion failed in CI while passing locally.  The harness now unsets the
inherited backend/pd environment; production precedence (env beats file) is
untouched and cases that mean to drive it from the env set it per invocation.

Not verifiable on this host: commons-configuration2 and snakeyaml were not
executed (no jars, no ~/.m2) -- the include spellings and the last-wins mapping
resolution follow imbajin's reproduction against the real parsers, and the
shell-side behaviour of each fix is measured here.

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 4 High severity · 1 Medium severity

Open (5)
What changed in this PR

Hardens Docker authentication bootstrap for mounted and upgraded HugeGraph configurations.

Changes:

  • Adds grammar-aware properties parsing and safe in-place rewriting.
  • Adds YAML authentication-state scanning and idempotent auth configuration.
  • Packages helpers and expands Docker entrypoint coverage.
File Description
hugegraph-server/​hugegraph-dist/​src/​assembly/​static/​bin/​props.awk Updated as part of this pull request.
hugegraph-server/​hugegraph-dist/​src/​assembly/​static/​bin/​enable-auth.sh Updated as part of this pull request.
hugegraph-server/​hugegraph-dist/​docker/​yamlscan.awk Updated as part of this pull request.
hugegraph-server/​hugegraph-dist/​docker/​test/​test-docker-entrypoint.sh Updated as part of this pull request.
hugegraph-server/​hugegraph-dist/​docker/​docker-entrypoint.sh Updated as part of this pull request.
hugegraph-server/​hugegraph-dist/​docker/​docker-entrypoint-test.sh Updated as part of this pull request.
hugegraph-server/​Dockerfile-hstore Updated as part of this pull request.
hugegraph-server/​Dockerfile Updated as part of this pull request.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +73 to +75
# props.awk needs no COPY: it ships in the assembly bin/ above, which is also
# where bin/enable-auth.sh finds it. yamlscan.awk serves only the entrypoint.
COPY hugegraph-server/hugegraph-dist/docker/yamlscan.awk .
Comment on lines +75 to +77
# props.awk needs no COPY: it ships in the assembly bin/ above, which is also
# where bin/enable-auth.sh finds it. yamlscan.awk serves only the entrypoint.
COPY hugegraph-server/hugegraph-dist/docker/yamlscan.awk .
# wait-partition.sh is how startup continued before partitions were assigned.
# Trailing whitespace is dropped here rather than in the reader, which reports
# the value verbatim apart from the escapes java.util.Properties resolves.
ACTUAL_BACKEND=$(get_prop_decoded "backend" "${GRAPH_CONF}" | tr -d '[:space:]' || true)
Comment on lines +183 to +185
if (v == "~") return 0
if (tolower(v) == "null") return 0
return 1
Comment on lines +172 to +179
if [[ -n "${rest_value}" ]]; then
rest=1
fi
if [[ "${state}" == "named" ]]; then
yaml=1
fi
if (( rest == yaml )); then
return 0

@imbajin imbajin left a comment

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.

Blocking: yes. Summary: Valid Gremlin authentication configurations can bypass the parity check, and an ambiguous bootstrap path can leave REST and Gremlin with different authentication states. Evidence: exact-head static review of yamlscan.awk, enable-auth.sh, and the entrypoint checks.

# valid to Settings.read() -- is recognized, while an `authentication:`
# nested under some other key is still not mistaken for the Gremlin one.
if (ROOT_IND < 0) {
if (!split_pair(line)) next

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.

‼️ Critical: A valid root flow mapping such as { host: 8182, authentication: { authenticator: org.example.Auth } } is accepted by TinkerPop 3.5.1's Settings.read()/SnakeYAML, but split_pair() captures only { host at the first colon. AUTH_BLOCKS stays 0 and this scanner returns none, so check_auth_sides() passes when REST has no authenticator even though Gremlin does. Please parse root flow mappings or fail closed whenever the root form cannot be classified.

if (ROOT_IND < 0) {
if (!split_pair(line)) next
ROOT_IND = ind
} else if (ind == ROOT_IND && in_auth) {

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.

⚠️ Important: In a valid multiline flow mapping (authentication: { with an indented authenticator: com.example.Auth and a closing }), the closing brace is at ROOT_IND. This branch clears flow before scan_flow() can commit the final pair, so the scanner reports nameless and check_auth_sides() refuses a valid authenticated configuration. Please let an open flow mapping consume its closing line before block-sibling handling, including a final item without a trailing comma.

# on it made split_pair see no colon followed by end of line, which reported
# a whole mapping as absent.
line = strip_comment($0)
sub(/[ \t\r]+$/, "", line)

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.

‼️ Critical: AWK splits records on LF, but this only strips CR characters at the end of an LF-delimited record. A valid bare-CR YAML file remains one record, so the root authentication: key is not recognized and AUTH_BLOCKS stays 0; REST can be left open beside authenticated Gremlin. Normalize CR, LF, and CRLF before parsing, or fail closed when the scanner cannot separate records.

# the last one (or are rejected), and answering from the first reported
# `named` for a server the empty second mapping left open.
if (ind == ROOT_IND && split_pair(line) &&
unquote(K_TXT) == "authentication") {

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.

‼️ Critical: YAML resolves double-quoted key escapes, so "authentic\u0061tion" is the server's authentication key. unquote() only removes surrounding quotes, so this comparison misses the key and returns none; with REST auth unset, the parity check accepts an authenticated Gremlin config beside open REST. Decode YAML escapes or reject quoted key forms the scanner cannot resolve.

[[ -f "${file}" ]] || return 1
if [[ -n "${YAMLSCAN}" ]]; then
state=$(awk -f "${YAMLSCAN}" "${file}") || fail "cannot read ${file}"
[[ "${state}" != "none" ]]

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.

‼️ Critical: This treats nameless as a usable authentication block. For an existing mapping without authenticator (for example, one that only sets authenticationHandler), enable-auth.sh skips the Gremlin update and still writes StandardAuthenticator to REST; TinkerPop 3.5.1 defaults an omitted authenticator to AllowAllAuthenticator. Please fail before changing REST or safely complete the mapping; the release-tarball fallback must also reject mappings it cannot verify.

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 b42b1ba. The guard now answers none/named/nameless through the shared reader and refuses before writing auth.authenticator when the mapping names no class, including the tarball grep fallback. New cases fail against c1dde5e; both CI-wired suites exit 0. No Docker or Java here, so no boot was observed.

Six findings from imbajin's round at 5940fa5 plus Copilot's block-scalar one.
Each input was run against the scanner at 5940fa5 and against the fixed one
before anything was claimed; the table is the evidence, and the new suite cases
revert red the same way.

  case                     5940fa5   fixed    note
  spread_flow_named        nameless  named    closing brace read as sibling
  escaped_root_key         none      named    "authentic\u0061tion" is the key
  escaped_child_key        nameless  named    same, on the direct child
  unresolvable_escape      none      nameless refused, not missed
  block_empty              named     nameless `authenticator: |` with no body
  root_flow                none      nameless refused, see below
  bare_cr                  none      named    one record, key never met

Controls measured unchanged: plain block named, nested mapping none, inline
flow named, handler-only block nameless, blank line inside a mapping named,
quoted scalar with a trailing comment named.  Both CI-wired suites exit 0, with
output byte-identical to the pre-change baseline apart from the new cases.

yamlscan.awk

- A flow mapping spread over several lines closed at the root indentation, and
  the sibling test fired before the flow branch could see the `}`: the pending
  entry was never committed, so a config naming a class read as nameless and the
  boot was refused.  The sibling test now yields while a flow is open.  Refusing
  a valid mounted config is its own bug, so the nameless-without-a-class form of
  the same layout is pinned as a control that the fix is not a blanket pass.
- unquote() compared quoted bytes without resolving them.  A double quoted
  scalar is unescaped by snakeyaml before it is a key, so "authentic\u0061tion"
  IS the authentication key and "authentic\u0061tor" the direct child; missing
  them answered none / nameless for a server that does authenticate, the
  direction this whole guard exists to close.  The documented escapes now decode
  through hexval(), and an escape this reader cannot resolve sets UNRESOLVED and
  refuses the document rather than quietly failing to match.
- CR was stripped at the end of a record only, so a file whose lines end at a
  bare CR arrived as one record whose root key was never met at all.  Records
  are split on CR now, which covers CR, LF and CRLF from one rule; the CR that
  the Linux reader leaves at the end of a CRLF record yields the empty segment
  the blank check already drops.
- names_class() judged a block scalar by its indicator, so `authenticator: |`
  with nothing behind it -- an empty string to the server, hence no
  authenticator -- reported named.  The deeper lines are now collected as the
  value, which keeps `authenticator: |` over a class name naming that class.
- A document written as one flow mapping is refused rather than classified.
  Parsing it means reading a depth-two authenticator behind a root key, and the
  wrong answer there is the silent one, so it takes imbajin's stated second
  option and fails closed with the reason on stderr.  This is a refusal of a
  form no shipped or documented config uses; say so if the block-form
  requirement is unwanted and the parser is the better half.

Not executed here, named rather than assumed: snakeyaml was never run, so the
escape-resolution and block-scalar readings follow the resolver's documented
behaviour and imbajin's reproduction against it, not a run of it; mawk is not on
this host, so the awk changes ran under GNU awk 5.0.0 only while CI reaches them
on Ubuntu; there is no Docker daemon, so no image was built and the mounted-
volume layout Copilot flags at Dockerfile:75 is untouched by this commit.
@Adarsh-Me

Copy link
Copy Markdown
Author

Fixed in c1dde5e — spread-flow closing brace, CR/LF/CRLF records, escaped quoted keys, empty block scalars. Each input run against 5940fa5 and the new head; table in the commit. Both shell suites exit 0. Root flow mapping is refused, not parsed. Dockerfile volume-mount and class-name parity untouched — no Docker here. gawk only; snakeyaml not run.

enable-auth.sh asked only whether an `authentication` mapping was there.  For
a mapping that names no authenticator -- one carrying just
authenticationHandler -- that answer sent it past the Gremlin append and
straight into writing auth.authenticator, so the script produced the exact
one-sided boot it exists to prevent: REST enforcing StandardAuthenticator
beside a Gremlin that authenticates nothing.  The entrypoint hides this today
because check_auth_sides refuses the same tree first, but the release tarball
ships enable-auth.sh with no yamlscan.awk and nothing in front of it, so the
refusal belongs here rather than leaned on the caller.

The guard now answers three ways through the same reader check_auth_sides
uses -- none, named, nameless -- because "is a mapping present" and "does it
name a class" are different questions and only the second decides what is safe
to write.  yamlscan.awk's refusal states (root flow mapping, unresolvable
escape, duplicate blocks) arrive as nameless and are refused here too instead
of being answered with a REST-only write.  In the tarball layout grep can only
see the key, so a mapping it cannot verify is refused unless the operator has
already named a class on the REST side, which is the one case where
ensure_rest_prop writes nothing and the tree is left as found.

Executed here: both CI-wired suites exit 0 at this commit --
test-docker-entrypoint.sh, and docker-entrypoint-test.sh with byte-identical
verdicts before and after.  The new assertions were run against the previous
script and fail with "enable-auth.sh must refuse a mapping that names no
authenticator", so they discriminate rather than track the fix.  enable-auth.sh
run by hand on a handler-only mapping in both layouts now exits 1 with
rest-server.properties still empty and gremlin-server.yaml and
hugegraph.properties byte-identical to before; against the old script the same
command exited 0 and left auth.authenticator written.  The suite was also run
from the staged LF blobs, not only the CRLF worktree, because CI checks out LF.

Not executed here, named rather than assumed: there is no Docker daemon and no
Java on this host, so no container booted and no server read the yaml -- that
TinkerPop 3.5.1 resolves an omitted authenticator to AllowAllAuthenticator is
imbajin's statement and his reproduction, not an observation of mine.  mawk is
absent, so this ran under GNU awk only.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: The b42b1ba change holds up. enable-auth.sh now refuses a nameless or unverifiable Gremlin mapping before it writes REST, in both the image layout and the tarball fallback. One new Minor item: an invalid AUTHENTICATOR_CLASS is written into the yaml before it is validated, so a refused run leaves a one-sided config. The open Copilot threads from 5940fa5 (helpers hidden by the /hugegraph-server volume, || true on the backend read, class-name parity) are still unanswered at this head and are not repeated. Evidence: full diff 83ef9f3..b42b1ba and c1dde5e..b42b1ba read; enable-auth.sh run against a copy of the shipped conf/ with AUTHENTICATOR_CLASS='com.example.My Auth' (exit 1, yaml block appended, REST untouched, yamlscan reports named); docker/test/test-docker-entrypoint.sh exits 0 on macOS with BWK awk; all six workflow runs at this head are action_required, so CI has not run.

if [[ "${GREMLIN_AUTH}" == "none" ]]; then
append_lines "${CONF}/${GREMLIN_SERVER_CONF}" \
'authentication: {' \
" authenticator: ${AUTHENTICATOR_CLASS}," \

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.

Minor: The yaml block is appended with AUTHENTICATOR_CLASS before anything checks that value, and props_set (line 97) only rejects it later, at line 239. A value that the character check refuses therefore leaves the one-sided tree that the comment above append_lines says this script must not leave.

Reproduced at b42b1ba on a copy of the shipped conf/ with the image layout (yamlscan.awk present):

$ AUTHENTICATOR_CLASS='com.example.My Auth' bin/enable-auth.sh
enable-auth.sh: refusing to write an unescaped value: com.example.My Auth    (exit 1)
gremlin-server.yaml   authentication: { authenticator: com.example.My Auth, ... }   yamlscan -> named
rest-server.properties  no auth.authenticator
hugegraph.properties    gremlin.graph=org.apache.hugegraph.HugeFactory

On the next container start check_auth_sides sees yaml named and REST empty and refuses to boot, and the operator has to edit the yaml by hand. A rerun of the script with the variable fixed or unset does not repair it either: the yaml is now named, so it keeps the bad class and REST gets StandardAuthenticator.

Requested change: validate AUTHENTICATOR_CLASS with the same character check right after line 164, before any file is touched, and add a case asserting that a refused class leaves both files unchanged.

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 a5de9f6 — the character check is now check_class_name, called as soon as AUTHENTICATOR_CLASS is set and before any config is touched; props_set still calls it. Your case exits 1 with both files unchanged and the yaml still none, and the rerun then arms both sides with one class. New both-layout assertions fail against b42b1ba. Suite exits 0; CI is still action_required.

The character check for a class name lived only in props_set, which first runs
at the rest-server.properties write -- one statement after the yaml block was
appended carrying that same ${AUTHENTICATOR_CLASS}.  A value the check rejects
therefore left the tree one-sided, which is the state this script exists to
prevent: AUTHENTICATOR_CLASS='com.example.My Auth' exited 1 with
gremlin-server.yaml naming com.example.My Auth (yamlscan.awk answers `named`)
while rest-server.properties stayed empty and hugegraph.properties kept the
plain HugeFactory.  The entrypoint's check_auth_sides reads that tree as
REST-unconfigured beside a Gremlin that names a class and refuses the boot, so
the refused run does not merely fail -- it arms a config the container will not
start in.

It also did not self-repair, which is what makes it worse than a messy failure:
once the yaml names a class the append is skipped, so re-running with the
variable unset wrote auth.authenticator=StandardAuthenticator to REST, exited 0
and reported success with the two servers configured for different
authenticators.  Measured on the same tree, not inferred.

The check is now one function, so the early refusal and the write cannot drift
apart, and it runs as soon as the value is known and before any config is
touched.  props_set still calls it; the yaml append can no longer be reached by
a value it would refuse.

Executed here: test-docker-entrypoint.sh exits 0 with the new assertions, and
exits 1 without the fix in both layouts it now covers -- "image layout
(yamlscan.awk present): a refused class still edited gremlin-server.yaml" and
"release tarball (no yamlscan.awk): a refused class still edited
gremlin-server.yaml" -- so they discriminate rather than track the fix.
docker-entrypoint-test.sh exits 0 with verdict lines identical before and
after.  enable-auth.sh run by hand with the bad class now exits 1 with both
files untouched and the yaml still `none`, and the following run with the
variable unset arms both sides with the same class; against the previous script
the same command left the yaml written and the rerun split.  Run from the
staged LF blobs, as CI checks out LF, under GNU awk 5.0.0.

Not executed here, named rather than assumed: the four host-gated assertion
groups (CRLF bytes, config-mode preservation, symlinked config, symlinked temp
file) skip on this Windows host and run only under CI, so this is again GNU awk
plus MSYS coreutils rather than mawk or BWK awk; there is no Docker daemon and
no Java here, so no container was booted and neither server read these configs
-- that check_auth_sides refuses the one-sided tree is shown by calling that
function on the tree the old script left, and what TinkerPop and
commons-configuration do with the result is bitflicker64's reproduction, not a
run of it.  The six workflow runs at b42b1ba are still action_required, so CI
has not run on this commit either.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: yes. Summary: yamlscan.awk reports named for a valid gremlin-server.yaml whose only authenticator sits inside a multi-line config flow mapping, so enable-auth.sh arms REST while Gremlin runs on AllowAllAuthenticator and check_auth_sides accepts it; two other valid flow spellings are reported as nameless and now refuse to boot. Evidence: ran head yamlscan.awk and enable-auth.sh against the shipped conf/ with the shapes in the inline comments, and parsed the same files with TinkerPop 3.5.1 Settings.read() on snakeyaml 1.27; test-docker-entrypoint.sh and docker-entrypoint-test.sh (GNU sed) both exit 0 locally; all six workflow runs at a5de9f6 are action_required, so CI has not run on this head.

# so `child` is always deeper than the root, as a real child must be.
if (!split_pair(line)) return
if (child < 0) child = ind
if (ind != child) return

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.

Critical: The block-mapping branch reads each line on its own, so a child value that opens a flow collection and continues on later lines is not tracked. A line inside that collection at the child indentation is then counted as a direct child. Example:

authentication:
  config: {tokens: conf/rest-server.properties,
  authenticator: org.apache.hugegraph.auth.StandardAuthenticator}

yamlscan.awk prints named for this file. TinkerPop 3.5.1 Settings.read() (snakeyaml 1.27) on the same file gives authentication.authenticator=org.apache.tinkerpop.gremlin.server.auth.AllowAllAuthenticator and config={tokens=conf/rest-server.properties, authenticator=org.apache.hugegraph.auth.StandardAuthenticator}, because flow content ignores indentation. Run against the shipped conf/ plus this mapping, enable-auth.sh exits 0, skips the yaml append and writes auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator to rest-server.properties. check_auth_sides then passes on every boot while REST enforces and Gremlin accepts unauthenticated requests. A double-quoted scalar that spans lines inside the mapping has the same effect.

Please track open flow collections and quoted scalars in child values and skip their continuation lines, or refuse through refuse() whenever a child value leaves one open. Add this case to test-docker-entrypoint.sh.

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 be7f5e2. Child values now track an open flow collection or quoted scalar and their continuation lines are skipped at any indentation, so config: {tokens: x, no longer lends its second line to the mapping; a span that never closes is refused. Test added to test-docker-entrypoint.sh; fails without the fix.

}
in_auth = 1
child = -1
if (substr(V_TXT, 1, 1) == "{") {

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.

Important: Only a value that starts with { on the key line is read as a flow mapping. Two valid spellings fall through to the block branch and are reported as nameless:

  • authentication: &auth {authenticator: org.apache.hugegraph.auth.StandardAuthenticator, authenticationHandler: org.apache.hugegraph.auth.WsAndHttpBasicAuthHandler} (an anchor before the flow mapping)
  • authentication: with {authenticator: org.apache.hugegraph.auth.StandardAuthenticator, authenticationHandler: ...} on the next line

TinkerPop 3.5.1 Settings.read() loads both with authenticator=org.apache.hugegraph.auth.StandardAuthenticator, and master boots them. At this head check_auth_sides runs on every start, so a container with either file and a matching auth.authenticator now refuses to boot, and the log says the mapping names no authenticator, which points the operator at the wrong fix.

Please drop a leading anchor (&name) before the { test and accept a flow mapping that opens on the first child line, or send these shapes through refuse() so the message says the shape is unsupported.

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.

Also fixed in be7f5e2: unanchor() strips a leading &label before the brace test, and a flow collection opening on the first child line goes to the same scan_flow() reader, so both spellings answer named the way Settings.read() loads them. Both shapes asserted in test-docker-entrypoint.sh.

yamlscan.awk answered `named` for a gremlin-server.yaml whose only
authenticator sits inside a multi-line flow mapping under `config`:

    authentication:
      config: {tokens: conf/rest-server.properties,
      authenticator: org.apache.hugegraph.auth.StandardAuthenticator}

Flow content ignores indentation, so the third line belongs to `config` and
Settings.read() hands the server the AllowAllAuthenticator default for
authentication.authenticator.  The block branch read each line on its own and
took that continuation line for a direct child, which is the unsafe direction
for this reader: on a tree carrying that file, enable-auth.sh exited 0, skipped
the yaml append because the reader said the mapping already named a class, and
wrote auth.authenticator=StandardAuthenticator to rest-server.properties;
check_auth_sides then passed on every boot, with REST enforcing and Gremlin
answering unauthenticated.  Measured rather than inferred -- the same tree now
exits 1 with rest-server.properties still empty and check_auth_sides refusing
the boot, which is what the guard is for.

A quoted scalar left open by a child value has the same effect, so it is
tracked the same way: the lines until it closes are nested content, at any
indentation, and never a direct child.  The span is ended where the collection
or the quote closes rather than at the end of the mapping, so a direct
`authenticator` written after a multi-line `config` still counts, and a
collection that never closes at all is refused through the nameless state, since
a file the server rejects has no answer this reader can give honestly.

Two valid spellings failed the other way and are fixed in the same pass.
`authentication: &auth {authenticator: X}` is a flow mapping behind an anchor,
and a flow mapping may open on the first child line instead of on the key line;
both load with the class named, and the `{` test on the raw value read neither,
so check_auth_sides stopped a container that master boots while reporting that
the mapping names no authenticator -- which points the operator at the wrong
line.  unanchor() now precedes the brace test, and a flow collection opening on
the first child line goes to the same scan_flow() that reads the other form,
because its braces hold the direct entries.

Executed here, from the staged LF blobs under GNU awk 5.0.0: every shape above
against both versions of the scanner -- five of the six new assertions return
the wrong state at a5de9f6 and the right one with the fix (the sixth, a direct
child after a closed span, answers `named` on both, so it guards against
over-swallowing rather than against this bug), and eight controls that must not
move did not (nested `config` on one line, a direct child, a null value, a
multi-line flow as the authentication value itself, a sequence child, a sibling
mapping with its own authenticator, a commented-out entry, no mapping at all).
test-docker-entrypoint.sh exits 0 with the new assertions and exits 1 with
yamlscan.awk reverted and the tests kept, failing on "expected yaml state
'nameless', got 'named'".  docker-entrypoint-test.sh exits 0 before and after.
bash -n and gawk --lint clean; staged blobs byte-checked for CRs, since this
clone has core.autocrlf=true.

Not executed here, named rather than assumed: the four host-gated assertion
groups (CRLF bytes, config-mode preservation, symlinked config, symlinked temp
file) skip on this Windows host and run only under CI; only gawk is installed,
so mawk and BWK awk are not exercised; there is no Docker daemon and no Java
here, so no container was booted and no server parsed these configs -- the
answers Settings.read() gives are bitflicker64's reproduction on snakeyaml 1.27
and not a run of it.  CI has not run on this commit: the runs at a5de9f6 were
still action_required when this was written.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: yes. Summary: A gremlin-server.yaml that master's enable-auth.sh appended to twice (bind-mounted conf, fresh /hugegraph-server volume, conf-bak missing) now stops the container on every boot, although TinkerPop 3.5.1 reads it as StandardAuthenticator. A valid mapping whose authenticator value starts on the next line is also refused, and an explicit-key ? authentication mapping reads as none. Evidence: ran the base enable-auth.sh twice over the shipped conf/, then the head docker-entrypoint.sh with PASSWORD set (exit 1, duplicate-mapping refusal); compared yamlscan.awk with TinkerPop 3.5.1 Settings.read() on snakeyaml 1.27 for 26 yaml shapes; test-docker-entrypoint.sh and docker-entrypoint-test.sh both exit 0 on macOS (BWK awk, GNU sed); all six workflow runs at be7f5e2 are action_required, so CI has not run on this head. The open Copilot threads (volume, || true on the backend read, class parity) are not repeated. The PR description still says the entrypoint propagates an authenticator to the missing side and warns on differing classes; the code now refuses one-sided configs and does not compare classes.

else if (SP > 0 || SQ != "")
RESULT = refuse("a flow collection or quoted scalar left open in the authentication mapping")
else if (AUTH_BLOCKS == 0) RESULT = "none"
else if (AUTH_BLOCKS > 1) RESULT = duplicate_root()

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.

Important: Refusing every file with two top-level authentication mappings stops containers that master's own enable-auth.sh produced. The base script appended a block whenever conf-bak/ was missing, and conf-bak lives in the /hugegraph-server volume, so each fresh container over a bind-mounted conf/ appended another block (the #3133 bug this PR fixes).

Reproduced: base enable-auth.sh run twice over the shipped conf/ with conf-bak removed in between gives 2 identical blocks. TinkerPop 3.5.1 Settings.read() (snakeyaml 1.27) on that file gives authenticator=org.apache.hugegraph.auth.StandardAuthenticator, and master boots it. At this head docker-entrypoint.sh with PASSWORD set prints 2 top-level authentication mappings and exits 1. The entrypoint's ERROR line then says the mapping "names no authenticator; add an authenticator entry", which points the operator at the wrong fix. check_auth_sides runs on every boot, so this also hits containers started without PASSWORD.

The test comment at test-docker-entrypoint.sh:1281 already records that Settings.read() takes the last mapping, and handle_line resets AUTH_SEEN/AUTH_NAMED when a later block opens. Requested change: answer from the last mapping here (else RESULT = auth_state()), which still refuses the existing test case whose last mapping is empty, and add a case with two identical StandardAuthenticator blocks that must read named. If you keep the refusal, make check_auth_sides print the duplicate-mapping reason instead of the "names no authenticator" text.

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 664dcac. Reproduced with the base enable-auth.sh run twice over the shipped conf/ (2 identical blocks), then TinkerPop 3.5.1 Settings.read() on snakeyaml 1.27 under JDK 17: authenticator=StandardAuthenticator. Now answers from the last mapping; a class-less last mapping still reads nameless. Entrypoint reaches start-hugegraph.sh instead of exiting 1.

BLOCK_TXT = ""
return
}
AUTH_NAMED = names_class(V_TXT)

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.

Important: A block authenticator whose value starts on the next, deeper line is reported as nameless, so a valid config that master boots now refuses to start. names_class gets the empty key-line text, and the deeper continuation line is skipped because its indentation is not child.

Measured against TinkerPop 3.5.1 Settings.read() on snakeyaml 1.27:

authentication:
  authenticator:
    org.apache.hugegraph.auth.StandardAuthenticator

yamlscan: nameless. Settings.read(): authenticator=org.apache.hugegraph.auth.StandardAuthenticator. The same holds with the value double-quoted on the next line. check_auth_sides then exits 1 with "names no authenticator", which is wrong for this file.

Requested change: when the key line carries no value, read the first deeper non-comment line as the value (plain or quoted scalar), or route this shape through refuse() so the message says the spelling is unsupported. Add both spellings to test-docker-entrypoint.sh.

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 664dcac. Both spellings measured against Settings.read(): plain and double-quoted value on the deeper line return StandardAuthenticator; the scanner said nameless and stopped the boot. It now waits for that line. A value there that is a mapping or collection names no class (the server throws) and goes through refuse(). Both added to the tests.

# EOF rather than exiting at the first: two top-level mappings resolve to
# the last one (or are rejected), and answering from the first reported
# `named` for a server the empty second mapping left open.
if (ind == ROOT_IND && split_pair(line) &&

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.

Minor: An explicit-key root mapping is not recognized, so a Gremlin config that authenticates reads as none. For ? authentication followed by : authenticator: org.apache.hugegraph.auth.StandardAuthenticator, yamlscan prints none, while TinkerPop 3.5.1 Settings.read() gives authenticator=org.apache.hugegraph.auth.StandardAuthenticator. With no auth.authenticator in rest-server.properties, check_auth_sides passes and REST starts open beside an authenticating Gremlin, the direction the entrypoint comments call unsafe. Master had no check here, so this is not a regression. Requested change: refuse any root line that starts with ? through refuse(), and add a test case.

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 664dcac, and the unsafe direction is confirmed: with rest-server.properties unconfigured this file reached start-hugegraph.sh at be7f5e2 while Settings.read() returns StandardAuthenticator. A root line beginning question-mark-space now goes through refuse(), so check_auth_sides stops the boot. Test added.

bitflicker64 reported three shapes where the scanner and the server disagree.
All three reproduce, and this time against the server itself: TinkerPop 3.5.1
Settings.read() on snakeyaml 1.27 (hugegraph-server/pom.xml tinkerpop.version,
TinkerPop 3.5.1 snakeyaml.version) run under JDK 17 on the files below, next to
this scanner.

1. Two top-level authentication mappings were refused outright.  That is the
file the base enable-auth.sh leaves behind: it appended a block inside
`if [ ! -d conf-bak ]`, and conf-bak/ is not on the mounted volume, so every
fresh container over a bind-mounted conf/ added another one (apache#3133, the bug this
PR fixes).  Running the base script twice over the shipped conf/ gives two
identical blocks, and Settings.read() on that file returns
StandardAuthenticator -- master boots it authenticated.  duplicate_root()
answered nameless, so check_auth_sides stopped the boot on every start, with or
without PASSWORD, and the entrypoint then told the operator to add an
authenticator entry the file already had.  The last mapping is now the one
answered, which is the node the server loads: handle_line() already cleared
AUTH_SEEN/AUTH_NAMED when a later block opened, so this only removes the
override that threw that work away.  A class-less LAST mapping still reads
nameless and still stops the boot, which is the direction that matters.

2. `authenticator:` with the class on the following deeper line is a folded
plain scalar to YAML, and Settings.read() returns the class.  The scanner read
the empty key line, answered nameless and refused a container that boots.  The
key now waits for that line, plain or quoted, and a value there that is a
mapping or a collection -- which names no class and makes the server throw
ConstructorException -- is refused rather than read as a class.

3. `? authentication` with its `: ...` value line is an explicit key, which this
reader does not walk.  It answered `none` while Settings.read() returned
StandardAuthenticator, and with rest-server.properties unconfigured
check_auth_sides saw two empty sides and let the container start: REST open
beside an authenticating Gremlin, the direction the file comments call unsafe.
It is refused now, through the same refuse() as the root flow mapping.

Executed here, from the staged LF blobs under GNU awk 5.0.0: 14 yaml shapes run
through both versions of the scanner AND through the real Settings.read().  Five
shapes where the server loads a class (two identical blocks, the base script's
own two-block output, a block whose second mapping names a class, a value on the
next line plain and quoted) returned nameless at be7f5e2 and named with the
fix; one explicit-key shape returned `none` at be7f5e2 and now refuses, which is
the safe answer for a file the reader cannot classify.  Six controls that must
not move did not: an empty LAST mapping after a named one (server
AllowAllAuthenticator -> still nameless), an authenticator left empty with a
sibling below (server null -> nameless), a nested mapping as the value (server
ConstructorException -> nameless), a single named mapping, a single null one,
and a duplicate authenticator child.  End to end, over a staged home with the
entrypoint and stubbed bin/: the duplicated file and the next-line value exit 1
at be7f5e2 with start-hugegraph.sh never reached and exit 0 with it called now;
the explicit-key file with the REST side unconfigured is the inverse -- it
REACHED start-hugegraph.sh at be7f5e2 (the unsafe boot) and exits 1 now.
test-docker-entrypoint.sh exits 0 with the seven new assertions and exits 1 with
yamlscan.awk reverted to be7f5e2 and the tests kept, failing first on "two
identical root mappings name the class: got nameless, want named";
docker-entrypoint-test.sh exits 0 before and after.  bash -n and gawk --lint
clean, staged blobs byte-checked at 0 CR (grep -c with a CR pattern is useless
under MSYS grep -- it matches every line; tr -cd '\r' | wc -c is the check).

Left alone on purpose: a duplicate `authenticator` key INSIDE one mapping is
still refused even though the server takes the last of those too.  It is not
what any shipped script produces, the two entries can name different classes,
and the existing message names the line to fix.  Say so if it should follow the
same rule as the root duplicate.

Not executed here, named rather than assumed: there is no Docker daemon and no
real server process, so nothing bound a port -- Settings.read() is the loader,
not the whole boot.  The jars (gremlin-server, gremlin-core, snakeyaml 1.27,
netty, slf4j) were fetched from Maven Central at the versions this repo declares
and run only to parse these files.  Only gawk is installed, so mawk and BWK awk
are not exercised; the four host-gated assertion groups still skip on Windows;
bitflicker64's other open threads (volume, `|| true` on the backend read, class
parity) are untouched, and the PR body still describes propagating an
authenticator to the missing side, which the code no longer does.  CI has not
run on this commit: the six runs at be7f5e2 were still action_required when it
was written.
@Adarsh-Me

Copy link
Copy Markdown
Author

All three fixed in 664dcac; both suites green here under gawk 5.0.0, and reverting just the scanner makes the new assertions fail. One asymmetry left deliberately: a duplicate authenticator inside one mapping still refuses, though the server takes the last of those too -- say if it should follow the root rule. CI is still action_required at this head.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: The awk YAML reader still gives the unsafe none answer for a root authentication key with a leading BOM, a !!str tag or an &anchor, and it refuses a valid CRLF block-scalar authenticator. Neither is a regression from master, and neither repeats an open thread (Dockerfile volume, the || true backend read, class-name parity, the stale PR description). Evidence: compared yamlscan.awk at 664dcac with snakeyaml 1.27 Yaml.load on nine yaml shapes; ran the head enable-auth.sh over the shipped conf/ with a BOM-prefixed authentication line; test-docker-entrypoint.sh and docker-entrypoint-test.sh both exit 0 locally (BWK awk 20200816, GNU sed 4.x); all six workflow runs at 664dcac are action_required, so CI has not run on this head.

# later one opens: two top-level mappings resolve to the LAST one, so the
# answer has to describe that node and not the first the scanner met.
if (ind == ROOT_IND && split_pair(line) &&
unquote(K_TXT) == "authentication") {

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.

Important: A root authentication key that SnakeYAML resolves but this comparison does not recognize reads as none, so the parity check passes while Gremlin authenticates. K_TXT is compared after unquote() only, so a UTF-8 BOM before a first-line key, a !!str tag on the key, or an &anchor on the key all miss it.

Measured at 664dcac with snakeyaml 1.27 (new Yaml().load) against awk -f yamlscan.awk:

<BOM>authentication: {authenticator: com.example.Auth}        snakeyaml: com.example.Auth   yamlscan: none
!!str authentication: {authenticator: com.example.Auth}       snakeyaml: com.example.Auth   yamlscan: none
&k authentication: {authenticator: com.example.Auth}          snakeyaml: com.example.Auth   yamlscan: none

Without PASSWORD, check_auth_sides sees REST unconfigured and yaml none, so the server starts with REST open beside an authenticating Gremlin. With PASSWORD, enable-auth.sh appends a second default mapping. I ran it over the shipped conf/ with the BOM line prepended: it exited 0, the yaml gained a second authentication: block, and SnakeYAML now resolves org.apache.hugegraph.auth.StandardAuthenticator, so the operator's class is replaced without a message. This matches what master does today, so it is not a regression, but it is the one-sided and duplicate-mapping outcome this reader exists to prevent.

Requested change: strip a leading U+FEFF from the first record, and send any root key whose text starts with !, & or * to refuse(), the same way the ? explicit-key form is handled now. Add the three rows above to test-docker-entrypoint.sh.

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 364d42f. BOM stripped from the first record; a node property before the authentication key goes to refuse(); an empty CR-split segment no longer closes a block scalar. Six assertions added, all checked against Settings.read(). Note: only that key is refused -- !!str host: beside a working mapping is an ordinary sibling, and refusing it stops a boot that works.

if (BLOCK) {
# Deeper than the key means the line is still scalar content; anything
# else ends the scalar and is ordinary content again.
if (indent_of(raw) > BLOCK_IND) {

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.

Minor: On a CRLF file a block-scalar authenticator always reads as nameless, so a valid config fails to boot. The main rule splits each LF record on \r, so authenticator: >-\r produces an empty segment right after the indicator line. That empty segment reaches this branch with indent 0, fails the > BLOCK_IND test, and finish_block() runs with an empty BLOCK_TXT before the content line is read.

Measured at 664dcac:

printf 'host: 0.0.0.0\r\nauthentication:\r\n  authenticator: >-\r\n    com.example.Auth\r\n'
snakeyaml 1.27: authenticator=com.example.Auth    yamlscan: nameless

The same file with LF endings reads named. check_auth_sides refuses every nameless answer, so the CRLF copy stops the container even with REST configured. It fails closed, so this is not a security problem.

Requested change: skip empty segments while BLOCK is open, or treat a blank line inside a block scalar as part of it the way YAML does. Add a CRLF block-scalar case next to the existing CRLF yaml test.

bitflicker64's 2026-09-28 review reported three shapes where this scanner and
the server disagree, and all three reproduce at 664dcac against the server's own
loader (TinkerPop 3.5.1 Settings.read() on snakeyaml 1.27 under JDK 17).

1. A UTF-8 byte order mark before the first key.  The mark frames the stream;
   SnakeYAML skips it, so "<BOM>authentication" IS the root mapping.  Compared
   byte-for-byte here, the mark made the key unknown, the answer was `none`, and
   with rest-server.properties unconfigured check_auth_sides saw two empty sides
   and started the container: REST open beside a Gremlin that authenticates --
   the one direction this reader exists to close.  strip_bom() now drops it from
   the first record, in whichever form the host's awk hands it over: the three
   bytes under a byte-oriented locale, the single code point under a multibyte
   one, each guarded by the length its own form has so it cannot mis-fire.

2. A node property in front of the key -- "!!str authentication",
   "&k authentication", "*a authentication".  SnakeYAML resolves the property off
   the key and builds the mapping; the reviewer measured the class back out of
   all three.  Resolving node properties is not what this reader does, so like
   the "? " explicit key the mapping it would open is refused rather than called
   unauthenticated.

   Narrowed against the literal request, deliberately: only a property in front
   of THAT key is refused.  "!!str host:" or "&defaults handler_pool:" is an
   ordinary root sibling, and Settings.read() takes no authentication mapping
   from it -- measuring the whole file, the version that refuses any root key
   starting with ! & * answered `nameless` for a file whose server side loads
   com.example.ShapeAuth, which stops a container whose two sides already agree.
   Refusing that was a new false positive, so the guard tests the key the answer
   depends on.  Say so if you want the literal form anyway.

3. A block scalar on a CRLF file.  Rule 7 splits each LF record at the CR, which
   leaves an empty segment after every line, and the block branch took that empty
   segment for the end of the scalar: "authenticator: >-" closed with no text
   before its content line arrived, so a Windows-saved config that does name a
   class answered `nameless` and check_auth_sides refused its own boot.  An empty
   line never ends a block scalar in YAML, so an empty segment now returns
   without closing it.  The reviewer called this "fails closed, so not a security
   problem", and it is still worth fixing: it is a valid config that cannot boot.

Executed here, from the staged LF blobs under GNU awk 5.0.0.  Fourteen fixture
files run through the real Settings.read() and through both versions of the
scanner: five shapes the server loads or rejects (BOM + flow, BOM + block, the
tag, the anchor, the alias, plus the tag-and-anchor pair) answered `none` or
`nameless` at 664dcac and now answer `named` or refuse; the CRLF block scalar
answered `nameless` at 664dcac while the server loads the class, and answers
`named` now.  Seven controls did not move: the LF and real-CRLF spellings of the
same block scalar, a plain named mapping, a plain none, a class-less mapping, the
AllowAll shipped conf, and both non-authentication siblings (tagged and anchored)
which stay `named` -- that last pair is what the narrowing above buys, and at
664dcac the tagged sibling was already right, so the broad guard would have
regressed it.  A 21-shape, two-locale table (LC_ALL=C and C.utf8, 42
measurements) passes 42/42 with the fix and fails 16 at 664dcac, all 16 inside
the reviewed shapes and none in a control.  Each new shape also runs through the
entrypoint's own yaml_auth_state: five WRONG at 664dcac, five RIGHT with the fix,
two sibling controls RIGHT at both.  End to end over a staged home with the
shipped conf and the reviewer's BOM line prepended: enable-auth.sh at 664dcac
took the file from one authentication mapping to two and rewrote the operator's
class to StandardAuthenticator silently; with the fix it stays at one mapping and
keeps the class, and the identical tree without the mark is 1 -> 1 at both
versions, so plain files behave exactly as before.
test-docker-entrypoint.sh exits 0 with the six new assertions and exits 1 with
only yamlscan.awk reverted to 664dcac, failing first on "byte order mark before
the root key is not part of it: got none, want named"; the new CRLF block-scalar
assertion sits inside the existing probe-gated CRLF group, which this host runs
rather than skips.  bash -n and gawk --lint clean, staged blobs byte-checked to 0
CR, the awk still holds no literal apostrophe.

Not executed here, named rather than assumed: there is no Docker daemon and no
Gremlin server process, so nothing bound a port -- Settings.read() is the loader,
not the boot.  Only gawk is installed, so mawk and BWK awk are not exercised; the
BOM's byte form and code-point form are covered by the two locales above, which
is the closest stand-in.  The snakeyaml and gremlin jars were reused from the
earlier local run of this harness, not re-fetched.  The four host-gated assertion
groups (CRLF byte check, config-mode preservation, symlinked config, symlinked
temp file) still skip on Windows and run only under CI.  CI has not run on this
commit: all four workflows at 664dcac are completed/action_required, which means
not run, and nothing here changes that.

@bitflicker64 bitflicker64 left a comment

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.

Blocking: no. Summary: Two small documentation and dead-code issues in props.awk at 364d42f; no new correctness problem found beyond the threads already open on this PR. Evidence: read the full diff at 364d42f, ran docker/test/test-docker-entrypoint.sh locally (passes), and ran props.awk get against a missing file and an include file (both exit 2).

# PROPS_MODE=get PROPS_KEY=K PROPS_FILE=F
# print the value of K's first logical definition, in the on-disk
# escaped form; with PROPS_DECODED=1 print it as java.util.Properties
# would hand it to the server. Always exits 0.

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.

Minor: The header says get always exits 0, but it exits 2 whenever props_load calls die: for an unreadable file (line 237) and for a file with an include directive (line 288). check_auth_sides in docker-entrypoint.sh and props_get in enable-auth.sh both rely on that nonzero status to refuse instead of treating the key as absent, so a reader who trusts this header could remove those checks.

Measured at 364d42f: PROPS_MODE=get PROPS_KEY=a PROPS_FILE=/nonexistent awk -f props.awk /dev/null prints props.awk: cannot read /nonexistent and exits 2. The same call on a file whose first line is include=x.properties also exits 2.

Please change the sentence to say that get exits 0 when the key is absent and 2 when the file cannot be read or uses an include directive.

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 9ae26c8: get now states exit 0 when the key is absent, 2 when the file cannot be read or uses an include — both reproduced at 364d42f. has, props_has and their tests are gone; nothing outside the suite called them. Its assertions became three get status checks, mutation-tested. Suite green here; CI has not run.

# empty one. Guards that append a default must not treat `auth.authenticator=`
# as absent, because appending a second definition leaves the empty first one
# in force under first-definition-wins.
function props_has(file, key, b) {

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.

Minor: No production code calls PROPS_MODE=has any more. enable-auth.sh replaced its props_has guard with ensure_rest_prop, which reads the value instead (its comment at enable-auth.sh lines 109-113 explains why the presence check was wrong), and the entrypoint only uses get and set. At 364d42f, git grep 'PROPS_MODE=has' matches only test-docker-entrypoint.sh (lines 1053, 1058, 1067, 1794).

The header text for this mode (lines 29-31 and 395-398) still presents it as the right guard before appending a default, which is the approach enable-auth.sh moved away from. Please remove the has mode, props_has and their tests, or point to the caller that needs them.

…ing calls

Two Minor findings from bitflicker64's 2026-09-28 review at 364d42f, both
reproduced before anything was changed.

1. The header promised "PROPS_MODE=get ... Always exits 0."  It does not:
   props_load dies for an unreadable file (props.awk:237) and for a file using a
   commons-configuration include directive (props.awk:288), both exit 2.  Measured
   at 364d42f exactly as the reviewer described: PROPS_KEY=a against /nonexistent
   prints "props.awk: cannot read /nonexistent" and exits 2, and the same call on a
   file whose first line is "include=x.properties" also exits 2.  The sentence now
   states the contract the callers actually depend on -- check_auth_sides
   (docker-entrypoint.sh:162-167) and props_get (enable-auth.sh:79-84) both treat a
   nonzero status as "the reader could not answer" and refuse, so a reader trusting
   the old wording could delete those guards and boot REST open beside a Gremlin
   that authenticates.

2. PROPS_MODE=has was dead code.  Nothing outside the test suite called it: at
   364d42f, grepping the whole hugegraph-dist tree finds has-mode invocations only
   in test-docker-entrypoint.sh, while docker-entrypoint.sh uses get and set and
   enable-auth.sh reads through props_get.  Its own header text presented it as the
   right guard before appending a default -- the approach ensure_rest_prop moved
   away from for the reason recorded at enable-auth.sh:108-116 (an empty
   definition is present to a status check and absent to the value check).  The
   mode, props_has and its tests are removed rather than kept "in case", and the
   two comments that named the function now name the check instead.

The suite's has-mode block becomes three get-mode assertions, so the status
contract corrected in (1) is executed rather than only documented: an empty
definition reads back empty, an absent key in a readable file exits 0, and an
unreadable file exits 2.  The include-refusal assertion for has is dropped, not
converted -- the read refusal one line above already covers it through
get_prop_encoded, and the write refusal below covers the other half.

Verified on this host (win32, Git Bash, gawk 5.0.0):
- docker/test/test-docker-entrypoint.sh exits 0 before and after, output identical
  apart from nothing at all, with the same 4 host-gated skips (symlink / CR / chmod
  groups this host cannot exercise, which run under CI).
- each new assertion shown to discriminate by mutating props.awk: die exit 2 -> 1
  fails the unreadable case; get exiting 1 on absence fails the absent-key case;
  printing before an empty value fails the empty-definition case.  Restored after
  each, no mutation committed.
- has mode confirmed gone, not merely unreferenced: PROPS_MODE=has exits 0 at
  364d42f and exits 2 with "PROPS_MODE must be get or set" here.
- repo-wide grep for PROPS_MODE=has / props_has returns 0 files; bash -n clean on
  both scripts; gawk --lint warnings 15 -> 13 with no new kind; git diff --check
  clean; committed blobs verified CR-free.

Not verified: CI.  Every run on this head is still completed/action_required, so
the Docker Build CI job that executes this suite has never run any of it -- the
approval was asked for twice already on this PR, so it is not asked a third time
here.  The document change in (1) has no failing-before test by nature; what is
tested is the status contract the corrected sentence describes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Docker entrypoint auth bootstrap is unsafe for mounted and upgraded configs

5 participants