`report:view` — the roadmap, the velocity and the six analytics reads, plus the three sprint analytics the sprint card hands over
Eleven project-scoped analytics reads get the key that says what they are. reportsService holds no access assertion on origin/main — every one of its reads reaches a gate, if at all, through whatever else the route happens to call, and /api/projects/[key]/velocity reaches none at all.
report:view is the one key in this story that is expected to be as wide as browsing — Jira has no separate report permission, and Browse Projects is what governs its aggregated reads. So this card is mostly a re-pointing that changes nobody's capability, with one exception: the actor who could reach a report on a project they cannot browse can no longer do so, because assertPermission refuses a non-browser with ProjectNotFoundError before it ever tests the key.
The eleven rows
| Operation | Service method |
|---|---|
GET /api/projects/[key]/roadmap | workItemsService.getProjectRoadmap |
GET /api/projects/[key]/velocity | reportsService.getVelocity — ungated today |
GET /api/reports/{average-age,created-vs-resolved,distribution,filter-results,resolution-time,workload} | reportsService.* |
GET /api/sprints/[id]/burndown | reportsService.getSprintCycleGraph |
GET /api/sprints/[id]/report | sprintsService.getSprintReport |
GET /api/sprints/[id]/points | estimationService.rollupForSprint |
The last three are handed over by the sprint card, which owns the sprint LIFECYCLE and deliberately does not own its analytics — so report:view has exactly one owning card and two cards never flip the same key.
Watch the project id. Several of these routes resolve a project from a key or from the active-project context, and one (filter-results) resolves it from the saved filter it runs. The gate takes a project ID, so a route that resolves the project only inside a repository call needs the resolution lifted before the assert — a gate that runs after the read is not a gate.
Acceptance criteria
- Each of the eleven service methods asserts
projectAccessService.assertPermission(projectId, ctx, 'report:view')before any data read, in the service and not the route. estimationService.rollupForSprintkeeps whateverestimation:managebehaviour the vocabularies card gave it, if any: this card adds the read gate and does not weaken an existing assert. Read that method before editing it, and say in the PR body which of the two it now holds and why.report:viewflips toenforcement: 'enforced'inlib/permissions/catalog.ts, withtests/permissions/catalog.test.ts's enforced list and length pin extended by exactly this key.- A unit test proves a project
viewerstill reads every one of the eleven (the key is browse-wide by decision), and that an actor who cannot browse getsProjectNotFoundErrorrather than a 403 — the no-existence-leak posture the other gates keep. filter-resultsresolves its project from the filter it runs, and a test proves a filter belonging to another project cannot be run through it.- The inventory rows record the key, and the guard's pinned counts are re-derived by running the guard on this branch.
pnpm test tests/permissions/ tests/services/is green.
Context refs
lib/services/reportsService.ts— no assertion today; the bulk of this card.lib/services/sprintsService.ts(getSprintReport),lib/services/estimationService.ts(rollupForSprint),lib/services/workItemsService.ts(getProjectRoadmap) — the three outliers.app/api/projects/[key]/velocity/route.ts— the one row that is ungated end-to-end.docs/decisions/member-facing-permissions.md— the decision thatreport:viewis browse-wide, and why.- The role-set seam · the sprint card — the two cards this one coordinates with.