Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2293Done

`assertPermission` — one gate per catalog key, its typed refusal, and the twelve administrative keys entering the built-in role sets

The seam every other card in this story calls. projectAccessService today has one administrative gate — assertCanManage, which asks project:administer — and the alternative to this card is growing eleven more of it (assertCanManageBoard, assertCanManageWorkflow, …), each a copy of the same three lines. One method that takes the KEY is the durable shape, and it is what custom roles needs too: a role is a set, so a gate should be a membership test the caller names.

This card also puts the twelve administrative keys into the built-in role sets, and that is where the story's neutrality claim is actually established. BUILTIN_ROLE_PERMISSIONS.admin gains all twelve; nothing else gains any. Because the resolution's levelGrants gives every key that is not work_item:edit / comment:add / attachment:create the same per-level treatment project:administer already gets, each new key resolves to exactly the actors project:administer resolves to, on every access level and both rails. That equivalence is provable over the whole input space and this card proves it.

Nothing is enforced here. Every key keeps enforcement: 'planned'; a key flips to enforced in the card that wires its gates. So this card is a no-op for every actor: it adds an unused method and widens a set nobody consults yet.

Acceptance criteria

  • projectAccessService.assertPermission(projectId, ctx, key, tx?) exists, takes a PermissionKey, and resolves the inputs ONCE. A non-browser is rejected as ProjectNotFoundError (404) FIRST — the no-existence-leak posture assertCanManage already keeps for settings surfaces — and a browser who does not hold the key is rejected with a typed error that carries the key and maps to 403.
  • The shipped wire contract does not change. Before choosing the error shape, git grep -n "NOT_PROJECT_ADMIN" -- app lib components tests e2e and keep every consumer of that string receiving it. The default shape is: a new PermissionDeniedError (carrying permission) in lib/projects/errors.ts, added to projectErrorResponse's 403 arm, thrown for every key EXCEPT project:administer, which keeps throwing NotProjectAdminError — a compatibility branch, documented as one, removable when nothing reads the old code.
  • assertCanManage is re-expressed as assertPermission(projectId, ctx, 'project:administer', tx), keeping its signature, its errors and its ordering. Every existing call site compiles and behaves identically, untouched by this PR.
  • ROLE_GATED_PERMISSIONS and BUILTIN_ROLE_PERMISSIONS.admin gain the twelve: member:manage, project:manage_access, board:configure, workflow:manage, automation:manage, field:manage, component:manage, label:manage, estimation:manage, repository:manage, repository:manage_access, ai:configure. member, viewer and IMPLICIT_WORKSPACE_MEMBER_PERMISSIONS gain none.
  • lib/permissions/resolve.ts's levelGrants is left behaving identically for the new keys, and the reason is written into the file: they take the same default branch project:administer takes, so private / limited subtract from them the same way.
  • The neutrality proof, in tests/permissions/accessParity.test.ts: across all 64 combinations of access level × workspace role × project role, hasPermission(i, k) equals hasPermission(i, 'project:administer') for each of the twelve. One table-driven assertion; a divergence anywhere fails it.
  • assertPermission carries its own unit tests: 404-before-403 ordering, the key present on the thrown error, an admin passing, a member refused, the tx threaded to the reads (the RLS binding the enclosing transaction holds).
  • No non-throwing hasPermissionOn / capability read is added. The five getXCapabilities methods stay exactly as they are — the surfaces that need a resolved set already have getPermissions, and what a client is TOLD is MOTIR-2258's.
  • pnpm test green; pnpm lint and the prettier check pass repo-wide.

Context refs

  • lib/services/projectAccessService.tsresolveInputs, assertCanManage (the 404-then-403 shape to reproduce), getPermissions, and the tx-optional convention every method here follows.
  • lib/permissions/builtinRoles.tsROLE_GATED_PERMISSIONS, BUILTIN_ROLE_PERMISSIONS, IMPLICIT_WORKSPACE_MEMBER_PERMISSIONS; its header records why the three public_request:* keys are in no role set.
  • lib/permissions/resolve.tsresolvePermissions, both rails, and levelGrants.
  • lib/permissions/catalog.tsPermissionKey, PERMISSION_META, PLANNED_PERMISSIONS.
  • lib/projects/errors.ts and lib/projects/projectErrorResponse.ts — the typed-error → status mapping, including the 403 arm.
  • tests/permissions/accessParity.test.ts — the 64-row table this card extends.
  • The model story · the roles-as-sets card · the catalog card.