Repository navigation
fix(portable): treat an empty OCI credential as absent instead of sending an empty bearer token - #1343
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
`oci_credential()` returned `Some("")` for a `GRAPHFORGE_OCI_CREDENTIAL`
that is exported but unset, because `std::env::var` yields `Ok("")` for it.
Nothing filtered that value, and `auth_header` routes any colon-free token
to the Bearer arm, so every OCI request carried the literal header
`Bearer ` with an empty token. An anonymous pull that should send no header
was rejected as an authentication failure, pointing the operator at the
wrong cause.
`oci_credential` now takes the raw value and maps empty or whitespace-only
input to `None`, matching the position `provider_openrouter` already takes.
Taking the value as an argument keeps the mapping a pure function, so it is
tested directly instead of through process-global environment mutation.
`HttpOciRegistry::new` applies the same normalization at the transport
boundary, and its control-character check is widened from CR and LF to all
control characters so the refusal matches the message it already reported.
The CR and LF refusal that blocks header injection is unchanged, and the
structured error class stays `InvalidPath`.
Scoped mutation score for `crates/graphforge-cli/src/portable_cli.rs`:
22 mutants, 14 caught, 3 missed, 5 unviable before; 23 mutants, 18 caught,
0 missed, 5 unviable after. All three former survivors were at
`oci_credential`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9efac10 to
a3751a0
Compare
Defect
oci_credential()incrates/graphforge-cli/src/portable_cli.rswasstd::env::var("GRAPHFORGE_OCI_CREDENTIAL").ok(). A variable that is exportedbut unset — the ordinary
GRAPHFORGE_OCI_CREDENTIAL="${REGISTRY_TOKEN}"shelland CI pattern — yields
Ok(""), so the function returnedSome("")ratherthan
None. Nothing downstream filtered it:HttpOciRegistry::newvalidatedonly for
\nand\r, andauth_headerroutes any colon-free token to theBearer arm. The result was the literal header value
Bearerwith an emptytoken on all six OCI request paths, so an anonymous pull from a public registry
that should send no header was rejected, and the failure surfaced as an
authentication error instead of a missing credential.
crates/graphforge-search/src/provider_openrouter.rsalready treats an emptybearer credential as absent; the OCI path was inconsistent with that precedent.
Secondary finding:
HttpOciRegistry::newrejected only CR and LF whilereporting
"credential contains control characters".Fix
oci_credentialnow takes the raw value and maps empty or whitespace-onlyinput to
None. Taking the value as an argument keeps the mapping a purefunction, so it is tested directly rather than by mutating process-global
environment state (
std::env::set_varis unsafe and racy under parallel testexecution). The env read moves to the two call sites behind a named
OCI_CREDENTIAL_ENVconstant.HttpOciRegistry::newapplies the same normalization at the transportboundary, so a direct API caller cannot produce an empty bearer token either.
\n/\rto all controlcharacters, so the check and its message agree. This strengthens rather than
weakens the refusal: CR and LF are still rejected, header injection is still
blocked, and the structured error class stays
PortableV2ErrorCode::InvalidPath.Basic/Bearer selection for non-empty credentials is unchanged, and present
credential values pass through untrimmed.
Mutation score
Scoped command from the issue:
The three prior survivors were all at
oci_credential— replacing it withNone, withSome(String::new()), and withSome("xyzzy".into()). All threeare now caught, along with the new
delete !mutant in the same function. Thefive unviable mutants are unchanged and unrelated (
Default::default()on typesthat do not implement
Default).Verification
crates/graphforge-cli/src/portable_cli.rscover absent,empty, whitespace-only, plain-token and colon-bearing inputs, plus the env
variable name, without touching process-global state.
crates/graphforge-portable-oci/src/tests.rsassert thecredential-to-header mapping across the same five input classes, prove over a
real loopback HTTP server that an empty credential sends no
Authorizationheader at all while the pull still succeeds, and prove that CR, LF, TAB and
other control characters are still refused with the matching message and error
class while an ordinary token is accepted.
than logging them, and the existing "errors never contain the credential"
tests still pass.
cargo test -p graphforge-cli -p graphforge-portable-oci --lib,cargo clippy -p graphforge-cli -p graphforge-portable-oci --all-targets(no new diagnostics),
cargo fmt --all -- --check, andmake pre-push-fastall pass locally. No new test files were added, so no Bazel target, migration
ledger, or parity changes were required;
bazel-migration-ledger-check.py,cargo-bazel-parity-check.py --mode inventoryandcargo-bazel-drift-check.pywere run and pass.Closes #1336
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.