Conversation
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
left a comment
There was a problem hiding this comment.
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.
imbajin
left a comment
There was a problem hiding this comment.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
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
left a comment
There was a problem hiding this comment.
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.
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.
|
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
left a comment
There was a problem hiding this comment.
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.
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 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 —
awkandgrepnever see a CR byte,chmodis a no-op (stat -c %areports 644 for a 600 file), andln -swrites 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
left a comment
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]*:/ { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 077plus the explicitchmod 600means a backup taken of a 0644 mounted config is never more permissive than its source, and repairs a stale.bakfrom a crashed run the same way the tmp path already does. The snapshot can holdauth.admin_pain clear.- Both staging files survive a failure on purpose: the
.tmpis what was being written and the.bakis 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.
|
One correction to what I wrote in the empty-fixture thread: I said Where the four open findings landed:
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 ( |
bitflicker64
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]*:/ { |
There was a problem hiding this comment.
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.Nestedget_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.
There was a problem hiding this comment.
Fixed in b8801a6, taking your first option: the key must start at column 0.
/^authentication[ \t]*:/ {
inblk = 1
have = 1so 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]) |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.WsAndHttpBasicAuthHandlerand 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 asnonerather than deciding the REST side; - the
both sides, different classescase passes through untouched with noWARN, as you described. Left as is deliberately: with the per-file guards already inenable-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.
|
Two things worth flagging rather than leaving in the diff: The proposal as sketched reopened the fail-open. Column 0 costs a test that used to pass. The old suite asserted 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 CI on |
imbajin
left a comment
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
| } | ||
| # 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 } |
There was a problem hiding this comment.
| # 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 } |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
| # 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 |
There was a problem hiding this comment.
| if [[ -s "${file}" && -n "$(tail -c 1 "${file}")" ]]; then | ||
| printf '\n' >> "${file}" | ||
| fi | ||
| printf '%s\n' "$@" >> "${file}" |
There was a problem hiding this comment.
| # 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) { |
There was a problem hiding this comment.
| 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 } |
There was a problem hiding this comment.
bitflicker64
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.Nestedand 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.
There was a problem hiding this comment.
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}"; thenTest 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 |
There was a problem hiding this comment.
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 getsauthenticator: org.apache.hugegraph.auth.StandardAuthenticator, and rest-server.properties keepsauth.authenticator=. - rest-server.properties has a bare
auth.authenticatorline. The check passes, and the file now holdsauth.authenticatorfollowed byauth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator.get_prop_encoded auth.authenticatorreturns 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.
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
|
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 ( You were right on all eight. Table of what each one was, measured against
The first, second and third were the same root cause and the worst of them: two of the three produced a false properties/yaml readers
One packaging change to look at: Two smaller changes the review pushed me into: Verification
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 CI at 53a5edb: six workflows registered, all |
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
left a comment
There was a problem hiding this comment.
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 != "~" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]/) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
.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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
includeOptionaland case-insensitive forms of both directives.props.awkrejects only exact lowercaseinclude. A real-parser fixture loads a REST authenticator from the included file whilecheck_auth_sidesreturns 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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Open (5)
Runtime helpers hidden by mounted application volume · New Runtime helpers hidden by mounted application volume · New props.awk errors are swallowed as empty backend · New Block scalar authenticators are misclassified as named values · New Authenticator parity check ignores differing class names · New
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.
| # 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 . |
| # 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) |
| if (v == "~") return 0 | ||
| if (tolower(v) == "null") return 0 | ||
| return 1 |
| if [[ -n "${rest_value}" ]]; then | ||
| rest=1 | ||
| fi | ||
| if [[ "${state}" == "named" ]]; then | ||
| yaml=1 | ||
| fi | ||
| if (( rest == yaml )); then | ||
| return 0 |
imbajin
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
{ 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) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
"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" ]] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Fixed in |
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
left a comment
There was a problem hiding this comment.
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}," \ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) == "{") { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) && |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
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 |
bitflicker64
left a comment
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.


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.shpreviously rewroterest-server.properties/hugegraph.propertieswithgrep/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 newprops.awkloaded by the entrypoint, which implements thejava.util.Propertiesline 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
psoutput. The oldsedrewrite interpolated the encoded value into sed's command line, so when a key already existed (mounted or persisted config) the password was visible inps. 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, customgremlin.graphfactories are preserved, and the authenticator class can be overridden viaAUTHENTICATOR_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
StandardAuthenticatorsplit the pair. When both sides name genuinely different authenticators, it logs a WARN and leaves both untouched instead of silently splitting them.Implementation notes
ConfigToolCLI: 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.awkships in both server images (Dockerfile COPY) and in the test sandbox; CI already runs the unit suite viadocker-build-ci.yml.How was this tested
docker/test/test-docker-entrypoint.shextended 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.shfull harness passes end-to-end (secret round-trips incl. backslash/space/trailing-space secrets, enable-auth call counting unchanged).gremlin.graphfactory (preserved).Code Review Handbook
props.awkis 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).docker-entrypoint-test.sh).auth\.admin_pascenario: the old grep could not match it, so the append created a duplicate and HugeConfig silently keptpa.Visual summary