git credentials: fail closed, and read a seat's token from its own slot
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
Two changes to one rule: a credential is resolved from exactly one place,
and an identity that cannot be resolved is refused rather than substituted.
FAIL CLOSED. Both readers ended in an unconditional fall-through to the
shared Gitea account whenever an identity did not resolve. Every seat in a
fleet therefore pushed, opened PRs and filed reviews under one account, and
a record made that way cannot be traced to the agent that made it
afterwards. The fallback now applies only where there is no attribution to
lose: a host with no fleet. Where seats exist, an unresolvable request emits
nothing, exits nonzero, explains itself on stderr, and — in the git helper —
appends a record naming the identity, host, reason and cwd, and no token
value, to ${MOSAIC_CREDENTIAL_SPOOL:-~/.local/state/mosaic-credential-escalations}.
A host runs a fleet when <brain>/fleet/agents exists, which is the signal
packages/mosaic/src/fleet/brain-home.ts already uses to decide a brain is
active, resolved the same way (MOSAIC_BRAIN_HOME, else ~/.mosaic). This is
what keeps the change a no-op for an operator who has not provisioned
per-slot tokens: no fleet directory, shared account, unchanged. It is also
why there is no environment variable to restore the old behavior — one would
reintroduce the substitution being removed.
STORE SELECTION. Both readers hardcoded ~/.config/mosaic/secrets/gitea-tokens,
so a seat's own secrets/ slot was invisible to the framework: a seat could
hold a valid credential and still be served the shared account. The store is
now chosen by what the identity is. An identity with a directory under
<brain>/fleet/agents/ is a seat and is read only from
<brain>/fleet/agents/<id>/secrets/; any other identity is a service identity
and is read from the framework store. There is no precedence between them
and no fallback from one to the other, so a seat with an empty slot is
refused even when a same-named token sits in the framework store. Two copies
of one credential are drift rather than redundancy, and drift surfaces as
the stale copy returning 401, which reads as a revoked token and sends
whoever debugs it somewhere else.
detect-platform.sh is in scope alongside git-credential-mosaic because they
are the two readers of these tokens. Patching only the git helper would make
"one credential, one location" true for push and fetch and false for
pr-create.sh, issue-create.sh and pr-review.sh, which is the harder failure
to notice.
TESTS. The three assertions that pinned the shared-account fall-through are
now fail-closed assertions, and a refusal is checked four independent ways:
nonzero exit, empty stdout, a stderr diagnostic naming identity and host,
and no shared token value anywhere in the output. The exit code alone would
pass against a helper that emitted the credential and then failed. Added:
seat-slot resolution, the no-cross-store-fallback case with a control
proving the framework-store file it declines to read is readable, no-identity
on a fleet host, the fleet gate firing on the default ~/.mosaic and not only
on an injected MOSAIC_BRAIN_HOME, and a cross-host leak check. Both suites
were run against the pre-change code as a control and fail there on exactly
the shared-token emission.
shellcheck is not installed on the authoring host, so the rewritten helper
is unlinted locally and CI is the first lint of it.
This commit is contained in:
@@ -23,6 +23,17 @@
|
||||
# 6. Scope containment: identity requested + an UNRECOGNIZED Gitea host (no
|
||||
# per-slot token scheme) -> Patch 2b does not apply; existing
|
||||
# fall-through behavior is unchanged.
|
||||
# 7. Credential store selection: an identity with a directory under
|
||||
# <brain>/fleet/agents/ is a SEAT and is read ONLY from its own secrets/
|
||||
# slot; any other identity is a SERVICE and is read from the framework
|
||||
# store. No precedence between them and NO fallback from one to the
|
||||
# other — a seat with an empty slot is REFUSED even when a same-named
|
||||
# token sits in the framework store.
|
||||
# 8. Fail loud when NO identity resolves on a host that runs a fleet: where
|
||||
# seats exist, an unattributable API call is refused rather than made
|
||||
# under the shared account. On a host with no fleet the same call still
|
||||
# returns the shared token (case 1), which is what keeps this change a
|
||||
# no-op for non-fleet operators of the framework.
|
||||
#
|
||||
# Uses a stubbed credentials.json + stubbed per-slot token files under a fake
|
||||
# HOME. NEVER reads real secrets or touches the real ~/.config/mosaic/secrets.
|
||||
@@ -210,6 +221,94 @@ if [[ "$err" == *"no per-slot token at"* ]]; then
|
||||
fi
|
||||
git -C "$REPO_DIR" config --unset mosaic.gitIdentity
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. SEAT identity: an id with a directory under <brain>/fleet/agents/ is read
|
||||
# from its OWN secrets/ slot, not from the framework store. The brain home
|
||||
# is resolved exactly as packages/mosaic/src/fleet/brain-home.ts does it:
|
||||
# MOSAIC_BRAIN_HOME, else ~/.mosaic.
|
||||
# ---------------------------------------------------------------------------
|
||||
BRAIN_DIR="$WORK_DIR/brain"
|
||||
mkdir -p "$BRAIN_DIR/fleet/agents/seatE/secrets"
|
||||
echo -n "seatE-slot-token" > "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token"
|
||||
out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatE MOSAIC_BRAIN_HOME="$BRAIN_DIR")
|
||||
assert_eq "seat reads its own slot" "seatE-slot-token" "$out"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. NO CROSS-STORE FALLBACK. seatF is a seat (it has a directory) with an
|
||||
# EMPTY slot, while a framework-store token of the identical name is
|
||||
# present and readable. It must be REFUSED rather than served that token:
|
||||
# one credential, one location. A seat that silently reads a same-named
|
||||
# service credential is the same substitution failure as the shared-account
|
||||
# fallback, one store further down.
|
||||
# ---------------------------------------------------------------------------
|
||||
mkdir -p "$BRAIN_DIR/fleet/agents/seatF/secrets"
|
||||
echo -n "seatF-SERVICE-STORE-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-seatF.token"
|
||||
assert_failloud "seat with empty slot does NOT fall back to the framework store" \
|
||||
"git.mosaicstack.dev" "seatF" \
|
||||
"$BRAIN_DIR/fleet/agents/seatF/secrets/gitea-mosaicstack-seatF.token" \
|
||||
MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$BRAIN_DIR"
|
||||
# assert_failloud only screens stderr for the word "shared"; this store's token
|
||||
# is not named that, so check for its value explicitly.
|
||||
set +e
|
||||
xstore_out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr-xstore.tmp")
|
||||
set -e
|
||||
if [[ "$xstore_out$(cat "$WORK_DIR/stderr-xstore.tmp")" == *"seatF-SERVICE-STORE-token"* ]]; then
|
||||
echo "FAIL: cross-store fallback — a seat was served the framework store's same-named token" >&2
|
||||
fail=1
|
||||
fi
|
||||
# Control: that framework-store token IS readable, so the refusal above is the
|
||||
# store rule firing and not an unreadable file. The same id, resolved against a
|
||||
# brain home where it is not a seat, gets it.
|
||||
out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=seatF MOSAIC_BRAIN_HOME="$WORK_DIR/no-such-brain")
|
||||
assert_eq "control — same file IS readable for a non-seat identity" "seatF-SERVICE-STORE-token" "$out"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 9. FAIL LOUD — no identity resolvable, but this host runs a fleet. Contrast
|
||||
# with case 1: the identical call on a host with no fleet still returns the
|
||||
# shared token.
|
||||
# ---------------------------------------------------------------------------
|
||||
git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true
|
||||
set +e
|
||||
noid_out=$(call_get_gitea_token "git.mosaicstack.dev" MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr-noid.tmp")
|
||||
noid_rc=$?
|
||||
set -e
|
||||
noid_err=$(cat "$WORK_DIR/stderr-noid.tmp")
|
||||
if [[ "$noid_rc" -eq 0 ]]; then
|
||||
echo "FAIL: no identity on a fleet host — expected nonzero return, got 0 (stdout='$noid_out')" >&2
|
||||
fail=1
|
||||
fi
|
||||
if [[ -n "$noid_out" ]]; then
|
||||
echo "FAIL: no identity on a fleet host — expected empty stdout, got '$noid_out'" >&2
|
||||
fail=1
|
||||
fi
|
||||
if [[ "$noid_out" == *"shared-mosaicstack-token"* || "$noid_err" == *"shared-mosaicstack-token"* ]]; then
|
||||
echo "FAIL: no identity on a fleet host — the shared token was served anyway:" >&2
|
||||
echo "$noid_out$noid_err" >&2
|
||||
fail=1
|
||||
fi
|
||||
if [[ "$noid_err" != *"MOSAIC_GIT_IDENTITY"* ]]; then
|
||||
echo "FAIL: no identity on a fleet host — stderr does not say how to set an identity:" >&2
|
||||
echo "$noid_err" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 10. The brain home defaults to ~/.mosaic when MOSAIC_BRAIN_HOME is unset —
|
||||
# the fleet gate must fire on the default path too, not only on an
|
||||
# explicitly injected one. Case 1 ran before this directory existed; the
|
||||
# same call now refuses, which also proves case 1 was measuring the
|
||||
# no-fleet branch rather than passing for an unrelated reason.
|
||||
# ---------------------------------------------------------------------------
|
||||
mkdir -p "$FAKE_HOME/.mosaic/fleet/agents"
|
||||
set +e
|
||||
dflt_out=$(call_get_gitea_token "git.mosaicstack.dev" 2>"$WORK_DIR/stderr-dflt.tmp")
|
||||
dflt_rc=$?
|
||||
set -e
|
||||
if [[ "$dflt_rc" -eq 0 || -n "$dflt_out" ]]; then
|
||||
echo "FAIL: fleet gate did not fire on the default ~/.mosaic brain home (rc=$dflt_rc stdout='$dflt_out')" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "get_gitea_token identity resolution regression passed"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user