fix(cli): separate transport auth from operation payload for add_user/alter_user - #1873
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces dedicated authentication arguments (auth_username and auth_password) to the CLI operations, preventing payload fields (such as those in add_user or alter_user) from being mistakenly used as transport credentials. It also updates the CLI help documentation and adds unit tests to verify the priority of these credentials and that they are stripped from the request bodies. Feedback was provided regarding a potential credential cross-contamination risk where a partially specified higher-priority credential source could cause the fallback logic to incorrectly mix admin usernames with payload passwords, along with a suggested fix to guard the fallback.
|
Reviewed; no blockers found. The only change since the last review pass (commit 6c6bd5b) is documentation-only — HELP text noting the stale-token 401 behavior and a comment clarifying |
…/alter_user add_user/alter_user reused the same username=/password= CLI args as both the HTTP Basic auth credentials and the operation payload, so authenticating as an admin while creating/altering a different user was impossible non-interactively — the payload values won the auth race, and env-var auth (HARPER_CLI_USERNAME/PASSWORD) was silently overridden. Give transport auth its own namespace (auth_username=/auth_password=), always preferred for the auth leg, and add them to TRANSPORT_ONLY_FIELDS so they never serialize into the request body. Env-var auth now also beats plain username=/password= when no dedicated auth arg is set, so a configured CI login isn't shadowed by an operation's own payload fields; username=/password= remain the auth fallback for ops where they legitimately ARE the auth. Also fixes the non-deploy body path, which sent the raw, unstripped req object as the wire body. Refs #1872 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…fields Follow-up to the review of the non-interactive auth fix. Three changes: - The saved `harper login` token now outranks the legacy `username=`/`password=` payload fallback. Previously any payload `username=` was folded into the target before the Authorization mode was picked, so `harper login` followed by `add_user username=newuser password=...` authenticated as the user being created and 401'd, leaving the interactive path with the original collision. - Credentials are resolved as an all-or-nothing pair from one source (dedicated args, URL userinfo, or an env-var namespace) instead of per-field `||` chains, which could pair one source's username with another's secret. An incomplete explicit pair is a hard error; an incomplete env pair warns and is skipped, since a lone HARPER_CLI_USERNAME/PASSWORD is a supported `harper login` idiom. `harper login` likewise no longer mixes the HARPER_CLI_* and CLI_TARGET_* namespaces, and prefers HARPER_CLI_* like the rest of the CLI. - Passwords no longer reach the logs: the parsed CLI request is redacted before the trace log, and a password embedded in `target=` is masked wherever the target is printed. Help text leads with the env-var form and notes that a password passed as an argument is exposed to shell history and CI logs. `harper login` now also sends its credentials as dedicated transport auth, so a re-login after expiry authenticates as the caller instead of trying to refresh the very token it is replacing. Refs #1872 Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
676990f to
7cbcd5a
Compare
DavidCockerill
left a comment
There was a problem hiding this comment.
Read through this carefully — the design is sound and the test coverage is genuinely strong (42 assertions across two files). No correctness bugs found. I traced the auth control flow, the atomic credential resolution, and the redaction paths, and confirmed utility/common_utils.ts httpRequest does no logging, so the Basic header can't leak downstream. The only programmatic caller (bin/login.ts) is updated, and the dormant .mjs test is describe.skip. Not requesting changes.
One thing I want to sign off on explicitly rather than let through silently, plus a few notes.
The auth-precedence change is a real behavior change on a stable CLI, and it deserves a conscious yes. Env-var auth and the saved harper login token now outrank payload username=/password=, where the payload previously won (bin/cliOperations.ts:393).
In plain terms: if someone's CI was quietly leaning on username=admin password=… to authenticate — say for create_table — and that same shell also had HARPER_CLI_USERNAME exported for something else, the command now authenticates as the env var instead of the argument. Almost always that's the same admin and nothing visibly changes. But it's exactly the kind of silent "who am I authenticating as" shift that should be a deliberate decision on an auth path rather than a surprise.
It's intended, it's yours, and it's tested — so consider this me blessing it rather than flagging it. Recording it here so the decision is on the record if someone hits it later.
The docs follow-up matters more than usual because of that. New auth_username=/auth_password= args, env-var auth, and new precedence rules are all user-facing. Is the documentation-repo follow-up tracked yet? I'd rather it not lag far behind the merge.
Smaller notes, none blocking:
- Incomplete-env warning is noisy (
cliOperations.ts:171). Exporting a loneHARPER_CLI_USERNAMEis a documentedharper loginidiom (password prompted), but in a non-login op it emitsIgnoring incomplete credentials: …on stderr for that command and every later one in the shell. Consider suppressing it when a saved token or other complete source is going to satisfy the request anyway. redactCredentialsis field-name-based, not exhaustive (cliOperations.ts:225).SECRET_FIELDSis{auth_password, password}plus target-URL userinfo — right for this PR's scope, but any other secret-bearing arg (a token, a key) would still reachlogger.trace. Worth a comment saying so, so nobody assumes the trace log is safe by construction.- Stale-token edge worth a line in the help/docs (
cliOperations.ts:438). If a saved token exists and refresh fails with a non-401,Authorizationstays set to the expired Bearer, so the legacy payload-credential fallback is skipped and the request 401s — where payload creds used to work. That's a coherent consequence of making the token outrank the payload (i.e. the point of the fix), andharper logout/auth_*is the escape hatch — it just deserves to be written down. - On the asymmetry you flagged (incomplete explicit creds fatal, incomplete env creds warn-and-skip): agreed with your default. Failing every op after a bare
export HARPER_CLI_USERNAMEwould be worse. Blessing it rather than letting it pass unremarked. - Housekeeping: you noted
676990fff(theDatabaseTransaction.tstype widening) isn't part of this change and gets dropped on rebase — worth confirming it's gone at merge so it doesn't ride in on an auth PR.
Note on review coverage: this one got a Claude-only pass — both outside-model legs were unavailable at the time. Codex is working again now, so if you'd like a second opinion on the auth surface before merging, say the word and I'll run it.
— Reviewed by DAIvid (Claude Opus 5)
Follow-up to review of the non-interactive auth fix (PR #1873): - HELP text now notes that a saved harper login token outranks username=/password=, so a stale token that fails to refresh 401s rather than falling back to the payload creds — harper logout or auth_username=/auth_password= is the escape hatch. - Noted that SECRET_FIELDS is a by-name allowlist, not exhaustive, so any future secret-bearing arg needs to be added explicitly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed two of the small, non-blocking notes from @DavidCockerill's review in 6c6bd5b:
Left the "noisy incomplete-env warning" suggestion (suppress it when a saved token/fallback ends up authenticating anyway) as-is — deferring/suppressing that warning would contradict the existing The two inline review threads (Gemini + earlier Claude pass on atomic credential resolution) were already resolved in — Claude Sonnet 5 |
DavidCockerill
left a comment
There was a problem hiding this comment.
Approving at 6c6bd5b. The delta since my last pass is six lines of comment and help text across two files — zero logic change, CI green on Node 22/24/26.
Everything I raised is closed:
- The auth-precedence change is documented, and documentation#613 covers all three user-facing pieces — the
auth_*args, env-var auth, and the precedence order — plus stale-token recovery. That was my main ask, since the precedence shift silently changes who a command authenticates as. redactCredentials' field-name-based scope is now noted in a comment.- The stale-token fallback behaviour is written down.
- The stray
676990fffDatabaseTransaction.tscommit is gone; the merge-base is now its standalone fix on main.
Re-verified the security properties at this head: no credential reaches a log, redaction still applied, Basic header still not logged. I also chased and refuted one candidate of my own — a URL parse error potentially leaking target userinfo.
Only open item is the incomplete-env warning noise, which you deliberately deferred; the current test locks in always-warn, so it wants a design call rather than a quick change. Happy to file it as a follow-up.
— Reviewed by DAIvid (Claude Opus 5)
What / why
add_user/alter_userreused the sameusername=/password=CLI args as both the HTTP Basic auth credentials and the operation payload, so the CLI tried to log in as the user being created. That made non-interactive (CI/CD)add_user/alter_user— authenticating as an existing admin while creating/altering a different user — impossible. Env-var auth (HARPER_CLI_USERNAME/HARPER_CLI_PASSWORD) was also silently overridden by the payload params.Root cause and fix, entirely in
bin/cliOperations.ts:auth_username=/auth_password=that always win the auth leg, and added them toTRANSPORT_ONLY_FIELDSso they never serialize into the request body.username/passwordfromTRANSPORT_ONLY_FIELDS— they're legitimate operation-payload fields for several ops (add_user,alter_user,create_authentication_tokens), not just transport. They remain the auth fallback for ops where they legitimately are the auth (e.g. plaincreate_table ... username= password=), preserving backward compatibility.HARPER_CLI_USERNAME/HARPER_CLI_PASSWORD,CLI_TARGET_USERNAME/CLI_TARGET_PASSWORD) beats plainusername=/password=when no dedicatedauth_*arg is set — otherwise a configured CI login is silently shadowed by an operation's own payload fields. Confirmed with @kriszyp before implementing (this is a deliberate behavior change, called out in the issue).body = req), which sent the entire raw, unstrippedreqobject as the wire body — now uses the sameoperationFields()stripping the deploy paths already used, soauth_username/auth_password(andtarget/rejectUnauthorized/etc.) never leak into any request body.HELPtext (bin/harper.ts) to document the new args and env-var auth for the non-interactive path. Companion docs: HarperFast/documentation#613.Test plan
dedicated auth args vs operation payload fields (harper#1872)describe block inunitTests/bin/cliOperations.test.js: auth_username/auth_password win the Basic-auth header while add_user payload username/password stay in the body; backward-compat fallback to plain username/password when nothing else is configured; env-var auth beats payload username/password; auth args stripped from both the non-deploy JSON body and the multipart directory-deploy body.unitTests/bin/cliOperations.test.jssuite passes (30/30).test:unit:mainpattern): 3743 passing, 10 pre-existing failures inunitTests/components/globalIsolation.test.jsunrelated to this change (component sandbox path resolution, reproduces identically onmain).oxlintandprettier --checkclean on all three changed files.Notes for reviewers
npm run buildcurrently fails onmainwith a pre-existing, unrelated TypeScript error inresources/DatabaseTransaction.ts(Promise<number | void>not assignable toPromise<void>, from feat(analytics): record write-transaction commit latency #1688) — not touched by this PR,tscstill emits (noEmitOnErroris off) so this didn't block testing.Follow-up: review round 2 (commit
9473f61ba)Three issues from review, plus three found by an outside-model pass on the fix itself:
1. Saved
harper logintoken was still unreachable foradd_user/alter_user(blocker). The payload fallback was folded intotarget.usernamebefore the Authorization mode was chosen, andif (target?.username)selects Basic ahead of the saved-token branch — soharper loginfollowed byadd_user username=newuser password=...authenticated as the user being created and 401'd. Authentication now resolves in this order:auth_*args → target-URL userinfo →HARPER_CLI_*→CLI_TARGET_*)harper loginoperation tokenusername=/password=payload fallback — now requiring both, so a loneusername=(drop_user username=bob) stays payload rather than becoming half a credential2. Credentials are resolved as an atomic pair, never composed across sources.
resolveTransportCredentials()walks the sources and returns the first that supplies anything, soauth_username=admincan no longer be paired with anadd_userpayload password. A source counts as configured once either half is supplied, empty or not, so a blank CI variable doesn't read as "absent". An incomplete explicit source (args, URL userinfo) is a hard error; an incomplete env pair warns on stderr and is skipped, because a loneHARPER_CLI_USERNAME/HARPER_CLI_PASSWORDis a supportedharper loginidiom and shouldn't break every later operation in the same shell or CI job.3. Passwords no longer reach logs or terminal output. The parsed CLI request is redacted before
logger.trace, and a password embedded intarget=https://admin:secret@hostis masked in theConnecting to ...line and anywhere the target is logged. Help text now leads with the env-var form and states that a password passed as an argument is exposed to shell history, process listings and CI logs.Also fixed in
bin/login.ts(found while verifying the above):harper loginmixed env namespaces — username fromCLI_TARGET_USERNAME, password fromHARPER_CLI_PASSWORD— which is the same cross-pairing hazard. Whichever namespace supplies anything now owns both halves; a half it doesn't set is prompted for. This also flips login's namespace preference toHARPER_CLI_*-first, matching the rest of the CLI (only observable if both complete pairs are set to different values).Follow-up test plan
unitTests/bin/cliOperations.test.js: 42 passing. New coverage — saved token wins for bothadd_userandalter_userwhile payload creds stay in the body; lone payloadusername=is not auth;CLI_TARGET_*pair still authenticates; loneauth_username, loneauth_password, emptyauth_passwordand both-empty auth args all fail with a clear error; incomplete env pair warns and falls through to the saved token; URL password masked in the connection log;redactCredentials()unit tests.unitTests/bin/login.test.js: newcredential sourcesblock — no cross-namespace pairing,CLI_TARGET_*-only still works, credentials sent as dedicated transport auth.npm run test:unit:main: 3758 passing, 10 pre-existingunitTests/components/globalIsolation.test.jsfailures (verified identical with these changes stashed).oxlintandprettier --checkclean.Open question for reviewers
The asymmetry in 2 is a judgment call: incomplete explicit credentials are fatal, incomplete env credentials only warn. The alternative (fatal everywhere) would break
export HARPER_CLI_USERNAME=admin; harper login; harper <op>— every operation after the login would fail even though the saved token is perfectly good. Happy to make it uniformly fatal if reviewers prefer that.Note on the extra commit
676990fff676990fffis not part of this change. It's a one-line type widening inresources/DatabaseTransaction.ts(recordCommitLatency's parameter toPromise<unknown>) that fixes the pre-existingnpm run buildfailure from #1688, which was turning the downstream Next.js adapter integration check red on this PR for reasons unrelated to it. The same fix is queued as its own PR againstmain; once that lands, this commit is redundant and can be dropped in the rebase.Refs #1872
🤖 Generated with Claude Code