Skip to content

feat(contract): declare the authorization class of every operation - #873

Merged
remyluslosius merged 5 commits into
mainfrom
feat/contract-declares-every-gate
Sep 21, 2026
Merged

remyluslosius merged 5 commits into
mainfrom
feat/contract-declares-every-gate

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

Summary

CP bugs/doing/OW-011 (filed 2026-08-12; accepted for the GA cycle 2026-09-21). The contract now states the gate for all 159 operations, reads included.

Census reconciled against main a5a05056 before editing: 69 declared a permission; 68 enforced one in the handler and declared nothing; 11 required an identity only; 11 were anonymous without a declaration beyond the four x-anonymous-mutations entries. The 2026-08-12 census counted 43 undeclared mutations; today's 85 undeclared is the same defect with reads added, not a regression. No operation was found open.

Contract (system-rbac 2.4.0, C-12; C-10's text updated). Exactly one class per operation:

  • x-required-permission on 135 operations (66 added), each derived from the EnforcePermission constant the handler passes, with the exception review routes traced through reviewException.
  • x-requires-identity: true on 11 routes that read the identity and refuse an anonymous caller.
  • Anonymous: security: [] (5), x-anonymous-mutations (4, unchanged), and the new top-level x-anonymous-reads (7), each with the reason it is open and what it returns. The permission registry stays anonymous by founder decision; its entry says what it never returns.

Tests. AC-18 now checks 135 declarations against handlers (was 69). AC-29 walks every operation, requires exactly one class, checks identity routes read and refuse, checks allowlisted routes enforce nothing, and closes the arithmetic at 159. AC-30 calls the registry anonymously and asserts exactly categories, permissions, roles; only built-in roles, equal to the built-in set; no user, email, username, assignment, DSN or secret key; and a handler source that reaches no pool, users service or roles store. Mutation checks performed: a wrong declaration (getGroups → host:write), a missing declaration (getAlerts), and a gated read allowlisted as anonymous each fail by name.

Also carried. The scope_id description on CredentialCreateRequest deferred from #870 (regenerates a code comment). One format-only commit for three frontend files drifted on main (same reason as #872: the prettier hook checks the whole directory when schema.d.ts is staged).

Recorded, not changed. api-activity C-01 mandates 403 for an anonymous caller where every other route answers 401 auth.required; AC-29 accepts either refusal for that reason. Aligning it is a contract decision for the founder.

Guide. GET /api/v1/auth/me/permissions is a ninth anonymous read (introspection: is_anonymous: true and an empty list); #870 will be amended to say so rather than editing the same paragraph here.

Checks

make spec-check 121/121, 100%; make check-generated in sync; go test ./internal/server/ -run 'TestRBAC|TestAPI_Credentials', ./internal/auth/, ./internal/identity/ green; hooks passed.

Candidate impact

v0.8.0-rc.5 is immutable; contract metadata there is as it was.

Format-only. The pre-commit prettier hook checks the whole frontend
directory, so any commit that touches a frontend file is blocked by drift
that landed earlier on main (24057cb, 1507b79). No CI job runs this
check. No logic changes.
api/openapi.yaml now states the gate for all 159 operations, reads
included, so a reader learns which routes are guarded from the contract
rather than from handler source (CP bugs/OW-011, filed 2026-08-12,
accepted for the GA cycle 2026-09-21).

Census before this change, taken against main a5a0505: 69 operations
declared x-required-permission; 68 enforced a permission in the handler
and declared nothing; 11 required an identity and no permission; 11 were
anonymous with no declaration beyond the four entries on
x-anonymous-mutations. The 2026-08-12 census counted 43 undeclared
mutations; today's count of 85 undeclared operations is the same defect
with reads added, not a regression. No operation was found open.

Three classes, exactly one per operation (system-rbac 2.4.0, C-12):
x-required-permission, derived from the EnforcePermission constant each
handler passes (66 added; the exception review routes pass the constant
through reviewException); x-requires-identity true for the 11 routes that
read the identity and refuse an anonymous caller; and anonymous, which is
security [] or an entry with a reason on x-anonymous-mutations or the new
x-anonymous-reads. The seven anonymous reads each carry the reason they
are open and what they return; the permission registry stays anonymous by
founder decision and its entry says what it never returns.

Tests. AC-18 now verifies 135 declared permissions against the handlers
(was 69). AC-29 walks every operation, requires exactly one class, checks
an identity route reads and refuses, checks an allowlisted route enforces
no permission, and closes the arithmetic at 159. AC-30 calls the registry
anonymously and asserts exactly categories, permissions and roles, only
built-in roles, no user or assignment or secret key, and a handler source
that reaches no pool or store. Mutation checks: a wrong declaration, a
missing declaration, and a gated read allowlisted as anonymous each fail
by name.

Also carried: the scope_id description on CredentialCreateRequest that
the OW-065 documentation PR deferred here because it regenerates code.
Generated Go and TypeScript follow; the x- extensions add no code. The
detect-secrets baseline is the hook's own line-number refresh.

One contract inconsistency is recorded, not changed: api-activity C-01
mandates 403 for an anonymous caller where every other route answers 401
auth.required; AC-29 accepts either refusal for that reason.

CP: bugs/doing/OW-011
remyluslosius added a commit that referenced this pull request Sep 21, 2026
… introspection

GET /api/v1/auth/me/permissions answers an anonymous caller with
is_anonymous true and an empty list, found while classifying every
operation for OW-011 (#873). It exposes nothing the caller does not
already know; the guide's inventory of anonymous reads must still name
it.
@github-actions github-actions Bot added size/L and removed size/XL labels Sep 21, 2026
Merging main kept main's baseline, which preserved all 84 fingerprints
but left three line numbers pointing above where this branch's inserted
contract lines moved the findings (api/openapi.yaml 171 to 195,
server.gen.go 4593 to 4595 and 4731 to 4733). Rescanned with the pinned
detect-secrets 1.5.0 against the baseline: no finding added or removed;
three locations corrected.
@github-actions github-actions Bot added size/XL and removed size/L labels Sep 21, 2026
@remyluslosius
remyluslosius merged commit 2967138 into main Sep 21, 2026
14 checks passed
@remyluslosius
remyluslosius deleted the feat/contract-declares-every-gate branch September 21, 2026 23:25
remyluslosius added a commit that referenced this pull request Sep 22, 2026
The error section takes main's paragraph: every process-generated
error now carries the envelope (#871 merged), so this branch's
sentence saying three were plain text is dropped. The audit-export
limitation sentence stays until #872 lands.
remyluslosius added a commit that referenced this pull request Sep 22, 2026
… tokens, pagination and errors (#870)

* docs(api): state what the API does today for logout, anonymous reads, tokens, pagination and errors

Eight factual corrections to the API guide, each checked against the
contract, the code or an annotated test, none changing a product promise
(CP bugs/OW-065).

- Anonymous reads: the guide named five credential-free operations and
  two anonymous reads; the handlers answer eight. The three it omitted
  (permissions:registry, sso/providers/enabled, the SSO redirect pair)
  are listed with what each returns and what the registry never returns.
- Credential scope: scope_id is required for host scope and forbidden
  for system scope (credential.validate, api-credentials AC-03). The
  matching description on the contract's create schema lands with the
  OW-011 contract change, which regenerates code under annotated tests.
- Pagination: the five cursor-paginated lists are named once, with the
  rule that next_cursor becomes the next request's cursor.
- Environment overrides: the loader is a fixed allowlist, not a generic
  form; the guide now says so and links the reference.
- Audit export: documented beside the list, with its filters, the
  10,000-row cap and the truncation header, and the fact that it drops
  correlation_id and ignores unknown parameters (bugs/OW-064).
- Errors: the three plain-text responses the service generates today
  are named (bugs/OW-063), and clients are told to expect non-JSON
  bodies from intermediaries regardless.
- Authentication: API tokens are the automation credential and password
  login is interactive; the example reads the password from a file, uses
  --fail-with-body so a 401 cannot become TOKEN=null, sets expires_at,
  and names the revoke route. One sentence on the CSRF header for
  cookie callers.
- Logout: the row says it revokes the cookies it is given and does
  nothing for a Bearer caller today (bugs/OW-062).

Unresolved behavior is described as unresolved and named by request;
nothing here presents it as fixed. The detect-secrets baseline is the
hook's own line-number refresh.

* docs(api): name the ninth anonymous read, the caller's own permission introspection

GET /api/v1/auth/me/permissions answers an anonymous caller with
is_anonymous true and an empty list, found while classifying every
operation for OW-011 (#873). It exposes nothing the caller does not
already know; the guide's inventory of anonymous reads must still name
it.

* docs(api): keep the export row inside the audit endpoints table

A blank line between the two rows split the table, so the export row
would have rendered as loose text.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant