`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.
| Operation | Service method | Today |
|---|---|---|
POST /api/import | importService.createDraft | assertCanEdit |
GET /api/import/[id] | importService.getImport | nothing |
POST /api/import/[id]/discover | importService.discoverFields | assertCanEdit |
POST /api/import/[id]/preview | importService.preview | assertCanEdit |
POST /api/import/[id]/run | importService.runImport | assertCanEdit |
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.assertCanEditcalls inlib/services/importService.tsbecomeassertPermission(projectId, ctx, 'import:run'), andgetImportgains the same assert — five in total, each before the method reads or writes. - A unit test proves a project
memberis now refused every one of the five with the typedPermissionDeniedError, and a projectadminpasses — 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:runflips toenforcement: 'enforced'inlib/permissions/catalog.ts, withtests/permissions/catalog.test.ts's enforced list and length pin extended by exactly this key.- The inventory's five project-scoped
R39rows 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.tson this branch. pnpm test tests/permissions/ tests/services/is green.
Context refs
lib/services/importService.ts— the fourassertCanEditsites and the ungatedgetImport/requireImportpair.lib/services/projectAccessService.ts—assertPermissionand 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 elevenR39rows, 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.