Skip to main content
← Back to list
01Issue
BugClosedSwamp CLITeam
AssigneesNone

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:

  1. A token holding every fine-grained scope in a family (datastore:setup, datastore:read, datastore:write) fails a gate that demands the literal datastore:*.
  2. Comparison is case-sensitive, and swamp auth token create accepts arbitrary scope strings verbatim, so Vault:* mints successfully and then never matches.
  3. #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--scopes handling is split(","), 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|expandScope returns 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 by datastore:* or by the fine-grained scopes it covers; a global * should satisfy everything. Reusing resourceSelectorMatches from domain/access/resource_selector.ts would 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 --scopes at 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.
  • #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 in a519305 (#1934); installed stable 20260722.210030.0-sha.7ff1e2f0 predates it.
  • macOS 14.
02Bog Flow
OPENTRIAGEDIN PROGRESSCLOSED

Closed

7/24/2026, 10:45:07 PM

No activity in this phase yet.

03Sludge Pulse
Editable. Press Enter to edit.

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:

  1. Repro B is reachable and unaffected. --scopes is 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 :*, or datastores:* 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.

  2. 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.

  3. 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.

stack72 commented 7/24/2026, 10:45:06 PM

@magistr Thanks for the detailed reports — we appreciate the work you're putting in to make the tool better. Fixes are in #1956 and will be released shortly.

Sign in to post a ripple.