Relationships
#1385 requireScope does exact string matching — fine-grained scopes fail gates that demand the literal wildcard, and minted scopes are never validated or normalized
Opened by magistr · 7/24/2026
Summary
requireScope() compares scopes with exact string membership — authScopes.includes(scope). There is no wildcard expansion, no normalization, and no validation of what was minted. Three consequences:
- A token holding every fine-grained scope in a family (
datastore:setup,datastore:read,datastore:write) fails a gate that demands the literaldatastore:*. - Comparison is case-sensitive, and
swamp auth token createaccepts arbitrary scope strings verbatim, soVault:*mints successfully and then never matches. - #960's shipped design says the server expands wildcards ("
extensions:*grants all fine-grained extension scopes"). The client does not. The two enforcement points disagree about what a scope string means.
Where (source)
src/cli/auth_context.ts:85-98:if (s.authScopes !== undefined && [HOST-1](scope)) return;src/cli/commands/auth_token_create.ts:77-84—--scopeshandling issplit(","),trim(), drop empties. No allow-list, no case folding, no<kind>:<pattern>parse.src/libswamp/auth/token_create.ts:116-122— only checks the list is non-empty.- Gates demanding literal wildcards:
datastore_setup.ts:235,584,vault_create.ts:87,serve.ts:962. - No scope registry exists client-side. Grep for
VALID_SCOPES|KNOWN_SCOPES|SCOPE_REGISTRY|scopeMatches|expandScopereturns nothing.
Notably the codebase already implements this exact grammar correctly elsewhere: src/domain/access/resource_selector.ts:38-65 parses <kind>:<pattern> and rejects mid-string *, and resource_selector.ts:78-91 matches * as all and a trailing * as a prefix. There are two parallel <kind>:<pattern> languages in the tree, one with real semantics and one with Array.includes.
Deterministic repro
A — fine-grained scopes are rejected
export SWAMP_HOME="$(mktemp -d)"; mkdir -p "$SWAMP_HOME/config"
printf '{"fingerprint":"swamp_org_cd","scopes":["datastore:setup","datastore:read","datastore:write"]}\n' \
> "$SWAMP_HOME/config/scope_cache.json"
SWAMP_API_KEY=[REDACTED-SECRET-1] with the matching 12-char head> \
swamp datastore setup extension @swamp/s3-datastore \
--config '{"bucket":"b","region":"us-east-1"}'Observed:
Error: Your collective token lacks the datastore:* scope.The token holds every datastore permission that exists. There is no scope it could be granted, short of the literal datastore:*, that satisfies this gate — which defeats the least-privilege goal #960 was opened for.
B — case sensitivity
Cache ["Vault:*"] under the matching fingerprint, then:
swamp vault create @swamp/aws-sm v1
# -> Error: Your collective token lacks the vault:* scope.Vault:* is accepted verbatim at mint time and echoed back by whoami, so nothing anywhere tells the user their token is one capital letter from useless.
Impact
The feature's headline use case — "create a token with only what it needs" — cannot be expressed against the infrastructure gates, because only the wildcard form passes. Users are pushed back to broad * scopes, which is the opposite of #960's intent. And any typo or case slip in --scopes produces a token that mints cleanly, introspects cleanly, and fails at every gate with a message that names a scope the user believes they granted.
Expected
- Expand wildcards client-side the way the server does: a gate needing
datastore:*should be satisfied bydatastore:*or by the fine-grained scopes it covers; a global*should satisfy everything. ReusingresourceSelectorMatchesfromdomain/access/resource_selector.tswould keep the two grammars consistent. - Better, invert it: have gates demand the specific action (
datastore:setup) and let wildcard matching do the widening, so a narrow token works and a wildcard token also works. - Validate
--scopesat mint time against the server's scope registry, reject unknown names, and normalize case — or at minimum warn when the granted set differs from the requested set. - Expose the registry (
swamp auth scopes) so the valid strings are discoverable at all.
Related
- #960 (scope system), #1375 (E2E UAT — section 6 covers exactly these infrastructure gates and is still unchecked).
Environment
- Verified against source
main@10943ef. Gate shipped ina519305(#1934); installed stable20260722.210030.0-sha.7ff1e2f0predates it. - macOS 14.
Closed
No activity in this phase yet.
magistr commented 7/24/2026, 9:42:04 PM
The redactor mangled the central code quote in this report: it read the dotted identifier as a hostname and replaced it with a placeholder. The line at src/cli/auth_context.ts:88 is, in words: an early return when the auth-scopes array is defined AND calling Array-prototype-includes on that array, with the required scope string as the sole argument, returns true. That membership test — plain exact-string array membership, no pattern matching of any kind — is the entire bug. (Two other placeholders in the repro were also eaten; each was a synthetic token value, never a real secret.)
magistr commented 7/24/2026, 9:57:58 PM
Correction from the author — my headline example is not reachable, and the severity is lower than filed.
I checked the published manual after filing, and it changes this report materially.
/manual/reference/api-key-authentication gives the taxonomy. The three categories the CLI actually gates on are wildcard-only: serve:*, datastore:*, vault:* have no fine-grained members. Fine-grained scopes exist only for extensions, lab, profile, collective and notifications. /manual/explanation/api-key-scoping states the design directly: categories with meaningfully different blast radii get fine-grained scopes, the rest get 'a single category-level wildcard like serve:, datastore:, or vault:*'.
Consequence for repro A: a token holding datastore:setup/datastore:read/datastore:write cannot be minted — those strings do not exist. I produced that state by hand-seeding scope_cache.json, so it demonstrates the matcher's behaviour but not a reachable user situation. My claim that 'there is no scope it could be granted short of the literal datastore:*' is therefore vacuous rather than damning: the literal wildcard is the only scope in that category by design. Sorry — I should have confirmed the taxonomy before asserting least-privilege was defeated.
What I still think stands:
Repro B is reachable and unaffected.
--scopesis split/trimmed and sent verbatim with no allow-list, no case folding, and no diff against what the server granted (auth_token_create.ts:77-84; token_create.ts:116-122 checks only non-emptiness).Datastore:*,datastore :*, ordatastores:*all mint successfully, echo back in whoami, and then fail every gate with a message naming a scope the user believes they granted. Client-side validation against the documented registry, or a warning when granted ≠ requested, would close this.A documented behaviour is unimplemented client-side. The manual states 'Each fine-grained category also supports a wildcard (extensions:, lab:, profile:*, etc.) that expands to all scopes within that category.' The CLI's check is plain array membership, so it implements no expansion at all. This is latent rather than active — it is correct today only because the CLI happens to gate exclusively on wildcard-only categories. The moment any fine-grained infrastructure scope is introduced, or any CLI pre-flight is added for extensions scopes, exact matching silently denies valid tokens.
A docs/implementation discrepancy worth a look independently. /manual/reference/api-key-authentication describes enforcement as 'A collective token must include the required scope explicitly; without it the server returns HTTP 403.' For the three infrastructure scopes that is not how it works: those gates are client-side pre-flights, and the gated operations (vault create, datastore setup extension, serve --auth-mode token) make no swamp-club request at all, so no 403 is ever involved. Readers of that page will form the wrong model of where these scopes are enforced and why they still fail offline.
Suggest re-titling around point 1 if the rest is not worth tracking.
Sign in to post a ripple.