Skip to content

chore(auth): Go permission constants are not enumerable, so the TS union stays a hand-maintained mirror #166

Description

@cristim

The frontend's Action and Resource union types in frontend/src/permissions.ts are hand-maintained mirrors of the Go constants in internal/auth/types.go. PR LeanerCloud/cloud-commitments-cli#1730 made the frontend side self-consistent — the runtime ALL_ACTIONS/ALL_RESOURCES arrays are now derived from those unions via a Record<Action, true> exhaustiveness check, so an array that drifts from the union fails tsc.

But the Go-to-TypeScript seam is still hand-maintained. Nothing detects the union drifting from Go.

That gap is what produced LeanerCloud/cloud-commitments-cli#1629 in the first place: the group-edit form's hardcoded <select> options had fallen 13 actions and 2 resources behind the backend, silently dropping permissions on save and — worse — widening view:history to view:*.

Current state (verified during the LeanerCloud/cloud-commitments-cli#1730 review)

The union and the Go constants are in exact agreement today: 20 actions, 11 resources, matching both directions. So this is a guard against future drift, not a live defect.

Why the obvious fix does not work

cmd/gen-permissions already generates from internal/auth and is CI-enforced through the permissions-codegen pre-commit hook, so extending it looks like the natural answer. It was investigated and it does not help as-is:

Go has no enumerable list of actions or resources — only individual const declarations. Adding a generator step therefore requires a new hand-maintained Go slice, which moves the seam rather than removing it. A hand-maintained Go slice that drifts from the Go consts is the same class of bug one layer down.

What would actually close it

Make the Go side enumerable at the source, then generate:

  • Add auth.AllActions() / auth.AllResources() returning the complete sets, with an exhaustiveness guarantee that does not rely on someone remembering to append. Options worth weighing: a go:generate step that parses the const block, a linter check that every Action-typed const appears in the slice, or restructuring the consts so the slice is the source and the individual names derive from it.
  • Then extend cmd/gen-permissions to emit the TypeScript unions, so tsc failing on drift becomes CI failing on drift.

Do not add a hand-maintained Go slice and call it done — that is the failure mode this issue exists to prevent.

Priority

Low urgency: the two sides agree today and LeanerCloud/cloud-commitments-cli#1730 makes the frontend internally consistent. But the cost of the drift when it recurs is a silent permission-corruption bug, which is why it is worth doing rather than closing as won't-fix.

Minor related nit: a comment introduced by LeanerCloud/cloud-commitments-cli#1730 says there is "exactly one place to update", which is true within the frontend but reads as repo-wide. Worth scoping when this is picked up.

Found during the adversarial review of LeanerCloud/cloud-commitments-cli#1730 (F3).

Findings from the 2026-09-02 codebase audit

Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.

A11-011 (medium)

A concrete drift instance for this issue, from finding A11-011. Besides the permissions.ts union this issue tracks, frontend/src/users/permissionMatrix.ts:11 keeps its own hand-written ACTIONS list of seven verbs, while ACTION_EXHAUSTIVENESS_CHECK in permissions.ts enumerates twenty. The matrix therefore renders no row for approve-any, retry-any, cancel-any, update-any, execute-any, sell-any, revoke-any or any -own variant, so an admin auditing which groups can approve a purchase sees dashes everywhere and concludes the capability is not granted. The group-edit form was already migrated onto ALL_ACTIONS for exactly this reason (see the drift comment at groupModals.ts:166-188); the matrix was not. The only test touching the file is an accessibility assertion, so nothing catches the drift.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions