MOTIR-2352Done
`saved_filter:manage` — authoring, owning, starring and subscribing to a project's saved queries
The saved-filter domain is the one member-facing key whose service already consults the access policy: savedFiltersService resolves projectAccessService.getSavedFilterCapabilities(project.id, ctx, tx) through a module-local helper and branches on the result. So this is a re-pointing, not a hole — with one genuine exception, /api/projects/[key]/saved-filters/[filterId]/subscription, which reaches no gate at all.
The shape, and the line it must not cross
- The WRITE methods —
create,update,delete,changeOwner,star,unstar, and the subscription write — assertprojectAccessService.assertPermission(projectId, ctx, 'saved_filter:manage'). - The READ methods —
list,resolve,getDependents— stay atproject:browse. Running a saved query is reading the project's work items, which every browser may already do. - The five
getXCapabilitiesmethods are NOT re-pointed here.assertPermission's card deliberately left them alone — "they are called from ~40 places; re-pointing them belongs with the surfaces that consume the set" — and that is the permission-gated UI story's work. This card adds the ASSERTION on the write path and leaves the capabilities read exactly as it is, so the two stories do not both edit the same method from opposite ends. - Ownership stays orthogonal. A filter has an owner, and
changeOwneralready has its own rule about who may reassign one; the permission answers may this actor manage saved filters in this project at all, and the ownership rule still applies on top. Read that method before adding the assert and keep both.
Acceptance criteria
- Each of the seven write methods asserts
saved_filter:managebefore its write, in the service. - The three read methods assert
project:browse, and a test proves aviewercan still list and run a saved filter while being refused authoring, starring and subscribing. - The subscription route's write reaches the gate — it is the one operation in this domain that reaches none today.
changeOwnerretains its existing ownership rule in addition to the new assert, and a test covers the case where an actor holdssaved_filter:managebut is not the filter's owner.saved_filter:manageflips toenforcement: 'enforced'inlib/permissions/catalog.ts, withtests/permissions/catalog.test.ts's enforced list and length pin extended by exactly this key.- No
getXCapabilitiesmethod is modified. - The inventory's five
R16rows record the key; the guard's pins are re-derived by running it on this branch. pnpm test tests/permissions/ tests/services/is green.
Context refs
lib/services/savedFiltersService.ts— the seven writes, the three reads, and the module-local capabilities helper.lib/services/projectAccessService.ts—assertPermission, andgetSavedFilterCapabilities(read it; do not change it).docs/decisions/permission-inventory.md— the fiveR16rows.docs/decisions/member-facing-permissions.md— the role assignment for this key.- The role-set seam — the prerequisite that makes the key resolve.
- The permission-gated UI story — the owner of the capabilities-read half.