feat(specverb): wildcard resource "*" in can/never grants — verb-global allow/deny #159

Closed
opened 2026-06-24 05:26:28 +00:00 by coilyco-ops · 1 comment
Member

Goal

Support a wildcard resource token "*" in can/never/cannot guardfile grants, so one grant applies a verb across all resources in the spec:

wrap ward-kdl ops forgejo {
    never delete "*"     // deny delete on every resource
    can get "*"          // allow get on every resource
    can list "*"
}

Today grants are per (modal, verb, resource) pair, so a verb-wide rule means hand-enumerating every resource — fragile, and it rots as the spec grows.

Motivation

  • ward#240 (3 ward-kdl forgejo builds: readonly / write / admin) needs this. readonly becomes can get "*" + can list "*" (everything else stays unmounted = denied); a no-delete admin build adds never delete "*". Without wildcards each build hand-lists every (verb,resource) across the whole forgejo spec.
  • Fail-closed future-proofing: a new spec resource with a delete op is auto-denied by never delete "*" — no guardfile edit. Matches the deny-by-structure direction (cli-guard#79).
  • DRY: collapses the repeated never delete <resource> lines in the forgejo / glitchtip guardfiles.

Semantics

  • can <verb> "*" — grant the verb for every resource in the spec that exposes it (expand in prune/resolve so all such ops mount).
  • never <verb> "*" / cannot <verb> "*" — deny the verb for every resource; mount the teaching leaf per blocked resource.
  • Precedence unchanged — deny beats allow (deny.go). So never edit "*" overrides a specific can edit issue; and a specific never delete org carves an exception out of a wildcard can delete "*" (allow-all-except-org). Wildcards and specifics compose through the existing deny-wins rule.
  • Default stays deny: unlisted (verb,resource) ops are not mounted (prune.go); can <verb> "*" only mounts ops that actually exist in the spec.
  • The wildcard grant carries the same block fields ({ message "..." }, { describe "..." }) and they apply to every expanded leaf.

Implementation (cli-guard http/specverb/)

  • guardfile package: accept "*" as a Grant.Resource sentinel.
  • deny.go (deniedKeys / denyDescriptors): a never <verb> "*" matches any resource for that verb — expand against the spec's resource set, or special-case Resource == "*" in the key matcher so a wildcard deny shadows all specific cans of that verb.
  • prune.go / resolve.go: a can <verb> "*" keeps/mounts every op of that verb across resources.
  • describe.go: render wildcard grants readably.
  • Lint: warn (don't silently drop) when a specific can <verb> X is shadowed by never <verb> "*", and vice-versa.

Acceptance criteria

  1. Parse + expand can <verb> "*" and never <verb> "*" against an OpenAPI spec; unit tests in specverb_test.go for both directions.
  2. Precedence tests: never <verb> "*" shadows a specific can <verb> X; a specific never <verb> X carves an exception out of can <verb> "*".
  3. Example proof: a readonly forgejo guardfile (can get "*" + can list "*") mounts only read leaves; a never delete "*" example blocks every delete leaf with the teaching message.
  4. describe renders wildcard grants. gen / skew clean. Reference doc updated. Conventional commit, closes this issue via the full Forgejo URL trailer, push to canonical main.

ward#240 (consumer: readonly/write/admin builds), cli-guard#79 (deny-by-structure), cli-guard#146 (specverb durability across specs).

## Goal Support a wildcard resource token `"*"` in `can`/`never`/`cannot` guardfile grants, so one grant applies a verb across **all** resources in the spec: ```kdl wrap ward-kdl ops forgejo { never delete "*" // deny delete on every resource can get "*" // allow get on every resource can list "*" } ``` Today grants are per `(modal, verb, resource)` pair, so a verb-wide rule means hand-enumerating every resource — fragile, and it rots as the spec grows. ## Motivation - **ward#240** (3 ward-kdl forgejo builds: readonly / write / admin) needs this. `readonly` becomes `can get "*"` + `can list "*"` (everything else stays unmounted = denied); a no-delete `admin` build adds `never delete "*"`. Without wildcards each build hand-lists every `(verb,resource)` across the whole forgejo spec. - **Fail-closed future-proofing:** a new spec resource with a `delete` op is auto-denied by `never delete "*"` — no guardfile edit. Matches the deny-by-structure direction (cli-guard#79). - **DRY:** collapses the repeated `never delete <resource>` lines in the forgejo / glitchtip guardfiles. ## Semantics - `can <verb> "*"` — grant the verb for every resource in the spec that exposes it (expand in prune/resolve so all such ops mount). - `never <verb> "*"` / `cannot <verb> "*"` — deny the verb for every resource; mount the teaching leaf per blocked resource. - **Precedence unchanged — deny beats allow** (`deny.go`). So `never edit "*"` overrides a specific `can edit issue`; and a specific `never delete org` carves an exception out of a wildcard `can delete "*"` (allow-all-except-org). Wildcards and specifics compose through the existing deny-wins rule. - **Default stays deny:** unlisted `(verb,resource)` ops are not mounted (`prune.go`); `can <verb> "*"` only mounts ops that actually exist in the spec. - The wildcard grant carries the same block fields (`{ message "..." }`, `{ describe "..." }`) and they apply to every expanded leaf. ## Implementation (cli-guard `http/specverb/`) - `guardfile` package: accept `"*"` as a `Grant.Resource` sentinel. - `deny.go` (`deniedKeys` / `denyDescriptors`): a `never <verb> "*"` matches any resource for that verb — expand against the spec's resource set, or special-case `Resource == "*"` in the key matcher so a wildcard deny shadows all specific `can`s of that verb. - `prune.go` / `resolve.go`: a `can <verb> "*"` keeps/mounts every op of that verb across resources. - `describe.go`: render wildcard grants readably. - Lint: **warn** (don't silently drop) when a specific `can <verb> X` is shadowed by `never <verb> "*"`, and vice-versa. ## Acceptance criteria 1. Parse + expand `can <verb> "*"` and `never <verb> "*"` against an OpenAPI spec; unit tests in `specverb_test.go` for both directions. 2. Precedence tests: `never <verb> "*"` shadows a specific `can <verb> X`; a specific `never <verb> X` carves an exception out of `can <verb> "*"`. 3. Example proof: a readonly forgejo guardfile (`can get "*"` + `can list "*"`) mounts only read leaves; a `never delete "*"` example blocks every delete leaf with the teaching message. 4. `describe` renders wildcard grants. `gen` / `skew` clean. Reference doc updated. Conventional commit, closes this issue via the full Forgejo URL trailer, push to canonical main. ## Related ward#240 (consumer: readonly/write/admin builds), cli-guard#79 (deny-by-structure), cli-guard#146 (specverb durability across specs).
Author
Member

🔒 Reserved by ward agent claude — container ward-cli-guard-issue-159-claude-dd2e131f on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-24T05:26:42Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-cli-guard-issue-159-claude-dd2e131f` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-24T05:26:42Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/cli-guard#159
No description provided.