fix(#1367): mint-seat-credential — secrets out of argv; discriminating pins (review 259 blocker, review 260 SF1-SF3)
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Blocker (rev-security-01 review 259): admin token, generated password and minted seat token travelled curl ARGV (three Authorization sites, -u at the mint, inline -d bodies), leaking to /proc cmdline and bash -x traces — the durable password under must_change_password:false is a live credential. All three now travel in 0600 staging files: --config for the Authorization header (the landed gitea_write_auth_config pattern), a user= directive for basic auth at the mint, --data @file for bodies. Unlinked after each use; M6 asserts no call is unauthenticated and no body is inline. Scope pin (SF2 + rev-security-01 M1, same defect): a mutant writing the REQUESTED scopes passed green because the grep target appears in both sets. M7 now asserts write:issue (requested, not granted) is ABSENT; mutant killed. SF3: hyphenated instance overrides map hyphen->underscore exactly like seat-logins.sh (url_override_var); M8 pins it; the uppercase-only mutant dies at the invalid-variable-name refusal again, now by design. SF1: mint-seat-credential.sh mode 755 (update-index), README invocation updated to name the now-required MOSAIC_SEAT_EMAIL_DOMAIN. Framework-PR firewall answer (rev-security-01): the email domain has NO default — unset is rc=3 with a named variable (M9); the instance host map stays per the seat-logins.sh precedent already on next. Estate domains belong to the estate, not the tree.
This commit is contained in:
@@ -13,11 +13,12 @@ Seat lifecycle tools for a Mosaic fleet. Paths are relative to
|
|||||||
## Onboarding a seat's credential
|
## Onboarding a seat's credential
|
||||||
|
|
||||||
```
|
```
|
||||||
MOSAIC_ADMIN_SEAT=<admin-seat> mint-seat-credential.sh <seat>
|
MOSAIC_ADMIN_SEAT=<admin-seat> MOSAIC_SEAT_EMAIL_DOMAIN=<domain> mint-seat-credential.sh <seat>
|
||||||
```
|
```
|
||||||
|
|
||||||
- The admin token is read from `$MOSAIC_BRAIN_HOME/fleet/agents/<admin-seat>/secrets/gitea-<instance>-<admin-seat>.token`. It is never printed.
|
- The admin token is read from `$MOSAIC_BRAIN_HOME/fleet/agents/<admin-seat>/secrets/gitea-<instance>-<admin-seat>.token`. It is never printed.
|
||||||
- Instances default to the map shared with `seat-logins.sh`; `MOSAIC_GITEA_INSTANCES="a b"` limits the set and `MOSAIC_GITEA_URL_<INSTANCE>` overrides a server URL.
|
- `MOSAIC_SEAT_EMAIL_DOMAIN` is required (no default): the framework ships no estate-specific domain.
|
||||||
|
- Instances default to the map shared with `seat-logins.sh`; `MOSAIC_GITEA_INSTANCES="a b"` limits the set and `MOSAIC_GITEA_URL_<INSTANCE>` overrides a server URL (hyphens in the instance name become underscores in the variable, as in `seat-logins.sh`).
|
||||||
- The seat slot is written from the mint response: `.token`, `.scopes` (what was granted), `.principal`, each mode 600.
|
- The seat slot is written from the mint response: `.token`, `.scopes` (what was granted), `.principal`, each mode 600.
|
||||||
- `tea` absent is a warning, not a failure: REST-path wrappers work with the token alone.
|
- `tea` absent is a warning, not a failure: REST-path wrappers work with the token alone.
|
||||||
- Regression suite: `test-mint-seat-credential.sh` (hermetic, mock curl, no network).
|
- Regression suite: `test-mint-seat-credential.sh` (hermetic, mock curl, no network).
|
||||||
|
|||||||
Regular → Executable
+63
-15
@@ -14,6 +14,10 @@
|
|||||||
# MOSAIC_GITEA_URL_<INSTANCE> server URL override per instance (same
|
# MOSAIC_GITEA_URL_<INSTANCE> server URL override per instance (same
|
||||||
# convention as seat-logins.sh).
|
# convention as seat-logins.sh).
|
||||||
# MOSAIC_SEAT_EMAIL_DOMAIN domain for the account email (<seat>@<domain>).
|
# MOSAIC_SEAT_EMAIL_DOMAIN domain for the account email (<seat>@<domain>).
|
||||||
|
# Required, no default: the framework tree
|
||||||
|
# carries no estate-specific domain
|
||||||
|
# (framework-PR firewall; the instance host
|
||||||
|
# map stays per seat-logins.sh precedent).
|
||||||
# MOSAIC_BRAIN_HOME brain checkout; default ~/.mosaic.
|
# MOSAIC_BRAIN_HOME brain checkout; default ~/.mosaic.
|
||||||
#
|
#
|
||||||
# Exit codes: 0 minted and projected on every instance; 1 at least one instance
|
# Exit codes: 0 minted and projected on every instance; 1 at least one instance
|
||||||
@@ -28,13 +32,43 @@
|
|||||||
#
|
#
|
||||||
# The .scopes file is written from the mint RESPONSE rather than from what was
|
# The .scopes file is written from the mint RESPONSE rather than from what was
|
||||||
# requested, so the record is what was granted rather than what was asked for.
|
# requested, so the record is what was granted rather than what was asked for.
|
||||||
|
#
|
||||||
|
# SECRETS NEVER TOUCH ARGV (#1343 class, rev-security-01 review 259): the admin
|
||||||
|
# token, the generated password, and the minted seat token all pass through
|
||||||
|
# 0600 curl --config / --data files — the landed in-tree standard
|
||||||
|
# (gitea_write_auth_config in detect-platform.sh). argv is world-readable via
|
||||||
|
# /proc/<pid>/cmdline for the life of each request, and a bash -x trace would
|
||||||
|
# print every secret otherwise. The staging files are unlinked after each use.
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Stage secrets into 0600 files; nothing secret reaches argv or a trace.
|
||||||
|
# write_auth_config <token> -> curl --config carrying the Authorization header
|
||||||
|
# (same shape as gitea_write_auth_config in
|
||||||
|
# detect-platform.sh, local so this script stays
|
||||||
|
# standalone under tools/fleet).
|
||||||
|
# write_user_config <u> <pw> -> curl --config with `user =` (covers -u).
|
||||||
|
# write_body <json> -> 0600 file for --data @file.
|
||||||
|
write_auth_config() {
|
||||||
|
local f; f=$(mktemp "${TMPDIR:-/tmp}/mosaic-mint-auth.XXXXXX") || return 1
|
||||||
|
printf 'header = "Authorization: token %s"\n' "$1" >"$f" || { rm -f "$f"; return 1; }
|
||||||
|
chmod 600 "$f"; printf '%s' "$f"
|
||||||
|
}
|
||||||
|
write_user_config() {
|
||||||
|
local f; f=$(mktemp "${TMPDIR:-/tmp}/mosaic-mint-user.XXXXXX") || return 1
|
||||||
|
printf 'user = "%s:%s"\n' "$1" "$2" >"$f" || { rm -f "$f"; return 1; }
|
||||||
|
chmod 600 "$f"; printf '%s' "$f"
|
||||||
|
}
|
||||||
|
write_body() {
|
||||||
|
local f; f=$(mktemp "${TMPDIR:-/tmp}/mosaic-mint-body.XXXXXX") || return 1
|
||||||
|
printf '%s' "$1" >"$f" || { rm -f "$f"; return 1; }
|
||||||
|
chmod 600 "$f"; printf '%s' "$f"
|
||||||
|
}
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BRAIN="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
|
BRAIN="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
|
||||||
ADMIN="${MOSAIC_ADMIN_SEAT:-}"
|
ADMIN="${MOSAIC_ADMIN_SEAT:-}"
|
||||||
INSTANCES="${MOSAIC_GITEA_INSTANCES:-}"
|
INSTANCES="${MOSAIC_GITEA_INSTANCES:-}"
|
||||||
EMAIL_DOMAIN="${MOSAIC_SEAT_EMAIL_DOMAIN:-mosaicstack.dev}"
|
EMAIL_DOMAIN="${MOSAIC_SEAT_EMAIL_DOMAIN:-}"
|
||||||
SEAT=""
|
SEAT=""
|
||||||
|
|
||||||
usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" >&2; exit 3; }
|
usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" >&2; exit 3; }
|
||||||
@@ -53,14 +87,18 @@ done
|
|||||||
[[ "$SEAT" =~ ^[a-z0-9][a-z0-9-]*$ ]] || { echo "mint: bad seat name: $SEAT" >&2; exit 3; }
|
[[ "$SEAT" =~ ^[a-z0-9][a-z0-9-]*$ ]] || { echo "mint: bad seat name: $SEAT" >&2; exit 3; }
|
||||||
[[ -n "$ADMIN" ]] || { echo "mint: no admin seat. Set MOSAIC_ADMIN_SEAT or pass --admin-seat." >&2; exit 3; }
|
[[ -n "$ADMIN" ]] || { echo "mint: no admin seat. Set MOSAIC_ADMIN_SEAT or pass --admin-seat." >&2; exit 3; }
|
||||||
[[ "$ADMIN" =~ ^[a-z0-9][a-z0-9-]*$ ]] || { echo "mint: bad admin seat name: $ADMIN" >&2; exit 3; }
|
[[ "$ADMIN" =~ ^[a-z0-9][a-z0-9-]*$ ]] || { echo "mint: bad admin seat name: $ADMIN" >&2; exit 3; }
|
||||||
|
[[ -n "$EMAIL_DOMAIN" ]] || { echo "mint: no email domain. Set MOSAIC_SEAT_EMAIL_DOMAIN (the framework ships no estate default)." >&2; exit 3; }
|
||||||
|
|
||||||
# Instance -> server URL. Same map and override convention as seat-logins.sh.
|
# Instance -> server URL. Same map and override convention as seat-logins.sh:
|
||||||
|
# hyphens in instance names map to underscores in the override variable
|
||||||
|
# (MOSAIC_GITEA_URL_MY-INST is not a valid shell name; MY_INST is).
|
||||||
|
url_override_var() { printf 'MOSAIC_GITEA_URL_%s' "$(printf '%s' "$1" | tr '[:lower:]-' '[:upper:]_')"; }
|
||||||
declare -A INSTANCE_URL=(
|
declare -A INSTANCE_URL=(
|
||||||
[mosaicstack]="https://git.mosaicstack.dev"
|
[mosaicstack]="https://git.mosaicstack.dev"
|
||||||
[usc]="https://git.uscllc.com"
|
[usc]="https://git.uscllc.com"
|
||||||
)
|
)
|
||||||
for inst in "${!INSTANCE_URL[@]}"; do
|
for inst in "${!INSTANCE_URL[@]}"; do
|
||||||
ov="MOSAIC_GITEA_URL_${inst^^}"
|
ov="$(url_override_var "$inst")"
|
||||||
[[ -n "${!ov:-}" ]] && INSTANCE_URL[$inst]="${!ov}"
|
[[ -n "${!ov:-}" ]] && INSTANCE_URL[$inst]="${!ov}"
|
||||||
done
|
done
|
||||||
[[ -n "$INSTANCES" ]] || INSTANCES="$(printf '%s\n' "${!INSTANCE_URL[@]}" | sort | tr '\n' ' ')"
|
[[ -n "$INSTANCES" ]] || INSTANCES="$(printf '%s\n' "${!INSTANCE_URL[@]}" | sort | tr '\n' ' ')"
|
||||||
@@ -71,31 +109,39 @@ mkdir -p "$D"; chmod 700 "$D"
|
|||||||
|
|
||||||
rc=0
|
rc=0
|
||||||
for KEY in $INSTANCES; do
|
for KEY in $INSTANCES; do
|
||||||
ov="MOSAIC_GITEA_URL_${KEY^^}"
|
ov="$(url_override_var "$KEY")"
|
||||||
BASE="${INSTANCE_URL[$KEY]:-${!ov:-}}"
|
BASE="${INSTANCE_URL[$KEY]:-${!ov:-}}"
|
||||||
[[ -n "$BASE" ]] || { echo " $KEY: no URL known for this instance (set $ov), skipped" >&2; rc=1; continue; }
|
[[ -n "$BASE" ]] || { echo " $KEY: no URL known for this instance (set $ov), skipped" >&2; rc=1; continue; }
|
||||||
ADMIN_TOKEN_FILE="$BRAIN/fleet/agents/$ADMIN/secrets/gitea-$KEY-$ADMIN.token"
|
ADMIN_TOKEN_FILE="$BRAIN/fleet/agents/$ADMIN/secrets/gitea-$KEY-$ADMIN.token"
|
||||||
[[ -r "$ADMIN_TOKEN_FILE" ]] || { echo " $KEY: no admin token for seat '$ADMIN' ($ADMIN_TOKEN_FILE), skipped" >&2; rc=1; continue; }
|
[[ -r "$ADMIN_TOKEN_FILE" ]] || { echo " $KEY: no admin token for seat '$ADMIN' ($ADMIN_TOKEN_FILE), skipped" >&2; rc=1; continue; }
|
||||||
T="$(cat "$ADMIN_TOKEN_FILE")"
|
T="$(cat "$ADMIN_TOKEN_FILE")"
|
||||||
|
AUTH_CFG="$(write_auth_config "$T")"
|
||||||
PW="$(openssl rand -base64 33 | tr -d '\n/+=' | head -c 32)"
|
PW="$(openssl rand -base64 33 | tr -d '\n/+=' | head -c 32)"
|
||||||
|
USER_CFG="$(write_user_config "$SEAT" "$PW")"
|
||||||
|
|
||||||
if curl -sf -o /dev/null -H "Authorization: token $T" "$BASE/api/v1/users/$SEAT"; then
|
if curl -sf -o /dev/null --config "$AUTH_CFG" "$BASE/api/v1/users/$SEAT"; then
|
||||||
curl -s -o /dev/null -X PATCH -H "Authorization: token $T" -H "Content-Type: application/json" \
|
BODY="$(write_body "{\"login_name\":\"$SEAT\",\"source_id\":0,\"password\":\"$PW\",\"must_change_password\":false}")"
|
||||||
-d "{\"login_name\":\"$SEAT\",\"source_id\":0,\"password\":\"$PW\",\"must_change_password\":false}" \
|
curl -s -o /dev/null -X PATCH -H "Content-Type: application/json" \
|
||||||
|
--config "$AUTH_CFG" --data "@$BODY" \
|
||||||
"$BASE/api/v1/admin/users/$SEAT"
|
"$BASE/api/v1/admin/users/$SEAT"
|
||||||
|
rm -f "$BODY"; BODY=""
|
||||||
act="reset-pw"
|
act="reset-pw"
|
||||||
else
|
else
|
||||||
curl -s -o /dev/null -X POST -H "Authorization: token $T" -H "Content-Type: application/json" \
|
BODY="$(write_body "{\"username\":\"$SEAT\",\"email\":\"$SEAT@$EMAIL_DOMAIN\",\"password\":\"$PW\",\"must_change_password\":false,\"full_name\":\"Mosaic fleet seat $SEAT\"}")"
|
||||||
-d "{\"username\":\"$SEAT\",\"email\":\"$SEAT@$EMAIL_DOMAIN\",\"password\":\"$PW\",\"must_change_password\":false,\"full_name\":\"Mosaic fleet seat $SEAT\"}" \
|
curl -s -o /dev/null -X POST -H "Content-Type: application/json" \
|
||||||
|
--config "$AUTH_CFG" --data "@$BODY" \
|
||||||
"$BASE/api/v1/admin/users"
|
"$BASE/api/v1/admin/users"
|
||||||
|
rm -f "$BODY"; BODY=""
|
||||||
act="create"
|
act="create"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"; chmod 600 "$tmp"
|
||||||
code="$(curl -s -o "$tmp" -w '%{http_code}' -X POST -u "$SEAT:$PW" -H "Content-Type: application/json" \
|
MINT_BODY="$(write_body "{\"name\":\"mosaic-seat\",\"scopes\":$SCOPES}")"
|
||||||
-d "{\"name\":\"mosaic-seat\",\"scopes\":$SCOPES}" "$BASE/api/v1/users/$SEAT/tokens")"
|
code="$(curl -s -o "$tmp" -w '%{http_code}' -X POST -H "Content-Type: application/json" \
|
||||||
|
--config "$USER_CFG" --data "@$MINT_BODY" "$BASE/api/v1/users/$SEAT/tokens")"
|
||||||
|
rm -f "$MINT_BODY"; MINT_BODY=""
|
||||||
if [[ "$code" != "201" ]]; then
|
if [[ "$code" != "201" ]]; then
|
||||||
echo " $KEY: mint FAILED http=$code ($act)" >&2; rm -f "$tmp"; rc=1; PW=""; continue
|
echo " $KEY: mint FAILED http=$code ($act)" >&2; rm -f "$tmp"; rc=1; PW=""; rm -f "$AUTH_CFG" "$USER_CFG"; continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 - "$tmp" "$D" "$KEY" "$SEAT" <<'PY'
|
python3 - "$tmp" "$D" "$KEY" "$SEAT" <<'PY'
|
||||||
@@ -109,10 +155,12 @@ p=pathlib.Path(d)
|
|||||||
for suf in ("token","scopes","principal"):
|
for suf in ("token","scopes","principal"):
|
||||||
(p/f"gitea-{key}-{seat}.{suf}").chmod(0o600)
|
(p/f"gitea-{key}-{seat}.{suf}").chmod(0o600)
|
||||||
PY
|
PY
|
||||||
rm -f "$tmp"; PW=""
|
rm -f "$tmp"; PW=""; rm -f "$AUTH_CFG" "$USER_CFG"
|
||||||
|
|
||||||
login="$(curl -s -H "Authorization: token $(cat "$D/gitea-$KEY-$SEAT.token")" "$BASE/api/v1/user" \
|
VERIFY_CFG="$(write_auth_config "$(cat "$D/gitea-$KEY-$SEAT.token")")"
|
||||||
|
login="$(curl -s --config "$VERIFY_CFG" "$BASE/api/v1/user" \
|
||||||
| python3 -c 'import json,sys;print(json.load(sys.stdin).get("login","ERR"))' 2>/dev/null || echo ERR)"
|
| python3 -c 'import json,sys;print(json.load(sys.stdin).get("login","ERR"))' 2>/dev/null || echo ERR)"
|
||||||
|
rm -f "$VERIFY_CFG"
|
||||||
if [[ "$login" == "$SEAT" ]]; then
|
if [[ "$login" == "$SEAT" ]]; then
|
||||||
echo " $KEY: $act, minted, GET /user -> $login"
|
echo " $KEY: $act, minted, GET /user -> $login"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -11,6 +11,17 @@
|
|||||||
# override MOSAIC_GITEA_URL_<INSTANCE> is honoured.
|
# override MOSAIC_GITEA_URL_<INSTANCE> is honoured.
|
||||||
# M4 no admin seat configured is a usage error (rc=3), nothing written.
|
# M4 no admin seat configured is a usage error (rc=3), nothing written.
|
||||||
# M5 the admin token value never appears on stdout or stderr.
|
# M5 the admin token value never appears on stdout or stderr.
|
||||||
|
# M6 secrets never touch argv: no Authorization header, no -u user:pass, no
|
||||||
|
# inline --data JSON carrying the password, on any curl invocation; auth
|
||||||
|
# travels in --config files and bodies in --data @files (#1343 class,
|
||||||
|
# rev-security-01 review 259 blocker).
|
||||||
|
# M7 the scopes record discriminates: a requested-but-not-granted scope
|
||||||
|
# (write:issue) must be ABSENT from .scopes — the pin is on the RESPONSE,
|
||||||
|
# and a mutant writing the requested set fails here (both reviewers).
|
||||||
|
# M8 hyphenated instance names resolve their override through the underscore
|
||||||
|
# variable, matching seat-logins.sh (SF3).
|
||||||
|
# M9 MOSAIC_SEAT_EMAIL_DOMAIN is required: unset is a usage error (rc=3),
|
||||||
|
# nothing written (framework-PR firewall answer).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
WORK_ROOT="${AGENT_WORK_ROOT:-${TMPDIR:-/tmp}}"
|
WORK_ROOT="${AGENT_WORK_ROOT:-${TMPDIR:-/tmp}}"
|
||||||
@@ -38,23 +49,34 @@ done
|
|||||||
export PATH="$MOCK_BIN:$SYS_BIN" CALLS
|
export PATH="$MOCK_BIN:$SYS_BIN" CALLS
|
||||||
export MOSAIC_BRAIN_HOME="$BRAIN"
|
export MOSAIC_BRAIN_HOME="$BRAIN"
|
||||||
export MOSAIC_GITEA_URL_ALPHA="https://alpha.example.test"
|
export MOSAIC_GITEA_URL_ALPHA="https://alpha.example.test"
|
||||||
|
export MOSAIC_SEAT_EMAIL_DOMAIN="seats.example.test"
|
||||||
unset MOSAIC_ADMIN_SEAT MOSAIC_GITEA_INSTANCES
|
unset MOSAIC_ADMIN_SEAT MOSAIC_GITEA_INSTANCES
|
||||||
|
|
||||||
# --- mock curl: records method + URL, answers the minting sequence -----------
|
# --- mock curl: records method + URL + a REDACTED auth marker, answers minting --
|
||||||
cat > "$MOCK_BIN/curl" <<'EOF'
|
cat > "$MOCK_BIN/curl" <<'EOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
method=GET; url=""; out=""; wcode=0
|
method=GET; url=""; out=""; wcode=0; auth=""; body=""
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-X) method="$2"; shift 2 ;;
|
-X) method="$2"; shift 2 ;;
|
||||||
-o) out="$2"; shift 2 ;;
|
-o) out="$2"; shift 2 ;;
|
||||||
-w) wcode=1; shift 2 ;;
|
-w) wcode=1; shift 2 ;;
|
||||||
-H|-d|-u) shift 2 ;;
|
--config)
|
||||||
|
if grep -q 'Authorization: token' "$2" 2>/dev/null; then auth="${auth}token,"; fi
|
||||||
|
if grep -q '^user = ' "$2" 2>/dev/null; then auth="${auth}user,"; fi
|
||||||
|
shift 2 ;;
|
||||||
|
--data)
|
||||||
|
case "$2" in
|
||||||
|
@*) body="@file" ;;
|
||||||
|
*) body="inline" ;;
|
||||||
|
esac
|
||||||
|
shift 2 ;;
|
||||||
|
-H|-u) shift 2 ;;
|
||||||
http*) url="$1"; shift ;;
|
http*) url="$1"; shift ;;
|
||||||
*) shift ;;
|
*) shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
printf '%s %s\n' "$method" "$url" >> "$CALLS"
|
printf '%s %s auth=%s body=%s\n' "$method" "$url" "${auth:-NONE}" "$body" >> "$CALLS"
|
||||||
emit() { if [ -n "$out" ]; then printf '%s' "$1" > "$out"; else printf '%s' "$1"; fi; }
|
emit() { if [ -n "$out" ]; then printf '%s' "$1" > "$out"; else printf '%s' "$1"; fi; }
|
||||||
case "$method $url" in
|
case "$method $url" in
|
||||||
"GET "*/api/v1/users/newseat) exit 22 ;; # 404 under -f: account does not exist yet
|
"GET "*/api/v1/users/newseat) exit 22 ;; # 404 under -f: account does not exist yet
|
||||||
@@ -77,6 +99,13 @@ rc=$(run newseat)
|
|||||||
grep -q 'MOSAIC_ADMIN_SEAT' "$SANDBOX/err" || fail "M4: error does not name MOSAIC_ADMIN_SEAT"
|
grep -q 'MOSAIC_ADMIN_SEAT' "$SANDBOX/err" || fail "M4: error does not name MOSAIC_ADMIN_SEAT"
|
||||||
[ ! -e "$BRAIN/fleet/agents/newseat/secrets/gitea-alpha-newseat.token" ] || fail "M4: a token was written without an admin seat"
|
[ ! -e "$BRAIN/fleet/agents/newseat/secrets/gitea-alpha-newseat.token" ] || fail "M4: a token was written without an admin seat"
|
||||||
|
|
||||||
|
# M9: email domain is required, unset is a usage error, nothing written.
|
||||||
|
rc=$(MOSAIC_ADMIN_SEAT=admin-seat MOSAIC_GITEA_INSTANCES=alpha MOSAIC_SEAT_EMAIL_DOMAIN= run newseat)
|
||||||
|
[ "$rc" = 3 ] || fail "M9: expected rc=3 with no email domain, got $rc: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q 'MOSAIC_SEAT_EMAIL_DOMAIN' "$SANDBOX/err" || fail "M9: error does not name MOSAIC_SEAT_EMAIL_DOMAIN"
|
||||||
|
[ ! -s "$CALLS" ] || fail "M9: API called without an email domain"
|
||||||
|
[ ! -e "$BRAIN/fleet/agents/newseat/secrets/gitea-alpha-newseat.token" ] || fail "M9: token written without an email domain"
|
||||||
|
|
||||||
# M1 + M3 + M5: mint on the single configured instance.
|
# M1 + M3 + M5: mint on the single configured instance.
|
||||||
: > "$CALLS"
|
: > "$CALLS"
|
||||||
rc=$(MOSAIC_ADMIN_SEAT=admin-seat MOSAIC_GITEA_INSTANCES=alpha run newseat)
|
rc=$(MOSAIC_ADMIN_SEAT=admin-seat MOSAIC_GITEA_INSTANCES=alpha run newseat)
|
||||||
@@ -92,7 +121,33 @@ grep -q 'alpha: create, minted, GET /user -> newseat' "$SANDBOX/out" || fail "M1
|
|||||||
grep -q 'https://alpha.example.test/api/v1/admin/users' "$CALLS" || fail "M3: URL override not honoured: $(cat "$CALLS")"
|
grep -q 'https://alpha.example.test/api/v1/admin/users' "$CALLS" || fail "M3: URL override not honoured: $(cat "$CALLS")"
|
||||||
if grep -q 'usc\|mosaicstack' "$CALLS"; then fail "M3: an instance outside MOSAIC_GITEA_INSTANCES was touched: $(cat "$CALLS")"; fi
|
if grep -q 'usc\|mosaicstack' "$CALLS"; then fail "M3: an instance outside MOSAIC_GITEA_INSTANCES was touched: $(cat "$CALLS")"; fi
|
||||||
grep -q 'tea not on PATH' "$SANDBOX/err" || fail "tea-absent path should warn, not fail: $(cat "$SANDBOX/err")"
|
grep -q 'tea not on PATH' "$SANDBOX/err" || fail "tea-absent path should warn, not fail: $(cat "$SANDBOX/err")"
|
||||||
if grep -q "$ADMIN_TOKEN_VALUE" "$SANDBOX/out" "$SANDBOX/err"; then fail "M5: admin token value leaked to output"; fi
|
if grep -q "$ADMIN_TOKEN_VALUE" "$SANDBOX/out" "$SANDBOX/err" "$CALLS"; then fail "M5: admin token value leaked to output or call log"; fi
|
||||||
|
|
||||||
|
# M7: scopes pin discriminates — requested-but-not-granted scope is ABSENT.
|
||||||
|
if grep -q 'write:issue' "$SLOT/gitea-alpha-newseat.scopes"; then
|
||||||
|
fail "M7: write:issue appears in .scopes — the record is the REQUESTED set, not the response"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# M6: no secret ever travels argv — every call authenticates via --config
|
||||||
|
# (token header or user= basic-auth directive) and bodies go as --data @file.
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "$line" in
|
||||||
|
*auth=NONE*) fail "M6: unauthenticated call: $line" ;;
|
||||||
|
*body=inline*) fail "M6: inline body (secret in argv risk): $line" ;;
|
||||||
|
esac
|
||||||
|
done < "$CALLS"
|
||||||
|
[ "$(grep -c 'auth=token' "$CALLS")" -eq 3 ] || fail "M6: expected exactly 3 token-auth calls (exists-check, admin write, verify), got: $(cat "$CALLS")"
|
||||||
|
grep -q 'auth=user' "$CALLS" || fail "M6: mint call did not use the user= directive: $(cat "$CALLS")"
|
||||||
|
|
||||||
|
# M8: hyphenated instance name resolves its override via the underscore variable.
|
||||||
|
printf '%s\n' "$ADMIN_TOKEN_VALUE" > "$BRAIN/fleet/agents/admin-seat/secrets/gitea-my-inst-admin-seat.token"
|
||||||
|
chmod 600 "$BRAIN/fleet/agents/admin-seat/secrets/gitea-my-inst-admin-seat.token"
|
||||||
|
export MOSAIC_GITEA_URL_MY_INST="https://myinst.example.test"
|
||||||
|
: > "$CALLS"; rm -rf "$BRAIN/fleet/agents/newseat"
|
||||||
|
rc=$(MOSAIC_ADMIN_SEAT=admin-seat MOSAIC_GITEA_INSTANCES=my-inst run newseat)
|
||||||
|
[ "$rc" = 0 ] || fail "M8: hyphenated instance mint failed rc=$rc: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q 'https://myinst.example.test/api/v1/admin/users' "$CALLS" || fail "M8: hyphen override (MY_INST) not honoured: $(cat "$CALLS")"
|
||||||
|
unset MOSAIC_GITEA_URL_MY_INST
|
||||||
|
|
||||||
# M2: admin token missing for the instance is reported, rc=1, nothing written.
|
# M2: admin token missing for the instance is reported, rc=1, nothing written.
|
||||||
rm -rf "$BRAIN/fleet/agents/newseat"
|
rm -rf "$BRAIN/fleet/agents/newseat"
|
||||||
|
|||||||
Reference in New Issue
Block a user