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-2353Done

`import:run` — the five project-scoped importer operations move from `work_item:edit` to an admin-only key

An import writes hundreds of work items into a project in one act, and today any project MEMBER can run one: importService asserts assertCanEdit(imp.projectId, ctx) in four places — the same permission as typing in a description field — and getImport asserts nothing at all. Both mirrors put this behind administration: Plane allows imports to workspace admins only, to maintain governance, and Linear requires a Linear Admin. This card gives the importer its own key.

Only five of the inventory's eleven import:run rows are in scope. The six OAuth legs bind a provider credential to a WORKSPACE and resolve no project, so the mapping card re-decides them before this one runs; wiring a project permission onto them is impossible, not merely wrong.

OperationService methodToday
POST /api/importimportService.createDraftassertCanEdit
GET /api/import/[id]importService.getImportnothing
POST /api/import/[id]/discoverimportService.discoverFieldsassertCanEdit
POST /api/import/[id]/previewimportService.previewassertCanEdit
POST /api/import/[id]/runimportService.runImportassertCanEdit

The read goes to import:run with the writes rather than to project:browse: an import draft holds the connection configuration and the field mapping, which is operator material for the person running the import, not project content for everyone who can see the board.

Acceptance criteria

  • All four projectAccessService.assertCanEdit calls in lib/services/importService.ts become assertPermission(projectId, ctx, 'import:run'), and getImport gains the same assert — five in total, each before the method reads or writes.
  • A unit test proves a project member is now refused every one of the five with the typed PermissionDeniedError, and a project admin passes — the member refusal is the revocation this card exists to make, so it is asserted rather than implied.
  • A test proves the refusal for a non-browser is still shaped as ProjectNotFoundError, not a 403.
  • import:run flips to enforcement: 'enforced' in lib/permissions/catalog.ts, with tests/permissions/catalog.test.ts's enforced list and length pin extended by exactly this key.
  • The inventory's five project-scoped R39 rows record the key; the six OAuth rows are left exactly as the mapping card left them and are not touched here.
  • The guard's pinned counts are re-derived by running tests/permissions/noUngovernedOperation.test.ts on this branch.
  • pnpm test tests/permissions/ tests/services/ is green.

Context refs

  • lib/services/importService.ts — the four assertCanEdit sites and the ungated getImport / requireImport pair.
  • lib/services/projectAccessService.tsassertPermission and its refusal ordering.
  • docs/decisions/member-facing-permissions.md — the admin-only assignment and the mirror evidence behind it.
  • docs/decisions/permission-inventory.md — the eleven R39 rows, six of which have already left.
  • The mapping card — the prerequisite that shrinks this domain to five.
  • The role-set seam — the prerequisite that makes the key resolve.