Skip to content

Classify an API endpoint's authorization (the M3b catalogue test)

Every routed API action and every public NotificationHub method must be catalogued. CI fails otherwise (EndpointAuthorizationCatalogTests in SyRF.API.Endpoint.Tests, part of application-authority milestone M3b of the transition plan).

What the test enforces

The inventory is MVC's own action discovery (the same application parts AddControllers maps), so a new controller or action is picked up without anyone listing it.

  1. A policy or an exception. Each action carries an [Authorize(<policy>)] whose policy the host registers (ProjectAuthorization, StageAuthorization, ApplicationAuthorization, the search-read policy or the named parity-read policy), or it has an entry in EndpointAuthorizationExceptions with a category and a reason. The global authenticated-user filter alone is not a classification.
  2. Anonymous and public endpoints are pinned by their response fields. For Anonymous and PublicMetadata entries the test computes every JSON field path of the declared success response ([ProducesResponseType], or the return type) and compares it with the listed paths. Adding a field to a public response fails CI until someone decides it is public. An action returning an untyped IActionResult cannot be pinned: its entry must say ResponseFields.Undeclared and describe the body in its reason (or declare [ProducesResponseType]).
  3. Gaps stay honest. A KnownGap entry must name a tracking reference (#…) and must still lack a policy: fixing it fails the test until the entry is deleted. An entry for an action that now has a policy, or no longer exists, also fails.
  4. The catalogue is consistent.
  5. Every project and stage policy resolves to an activity enum member with a deployed ResourceSecurity.json default. A policy that does not resolve denies everyone silently; a missing default is a 500.
  6. Every activity has a default, and every default names a known activity, once.
  7. Every application policy has a default.
  8. StageView and StageDelete are the two known unresolvable stage policies (#3335 D3).
  9. ProjectSearchReadPolicy names no single activity: each of its activities must be catalogued with a default.
  10. Named protected workloads (export creation and download, administrative email sends, batch Risk of Bias, PDF corrections, RoB writes, bulk-PDF report and path downloads) must carry a resource policy. Moving one into the exception table is itself a failure.

Search reads

Project View is open to every signed-in user, so it covers public project metadata only. A search's description and original reference files are not public metadata (SM-13).

  • Any action that answers SearchDtos needs ProjectSearchReadPolicy (ProjectSearchReadAuthorization), or the ViewSearches, ImportSearch or RemoveSearch policy. SearchContentActionsNeedASearchActivityPolicy fails otherwise. ProjectSearchReadPolicy admits an active member who holds any one of those three activities. Each activity is decided exactly as its own policy would decide it, in every application-authority mode, and the deleted-project and absent-project answers are unchanged. It changes no default grant.
  • Project details are built for every member. There ProjectJobFamilyPermissionFilter trims each search with SearchDto.WithoutSourceDetail() for a caller without one of those activities: id, name, study count and the living-search link stay; the description and reference files do not. The filter runs inside the mapping, so the project endpoints, the realtime dispatcher and the project hub all apply it, including searches preloaded from the materialised search-population read.
  • With studiesWorkspace on, the same filter withholds even names and counts unless the caller holds ViewSearchOverview ("Who can see search names", study management W4): such a caller gets no searches at all. ViewSearchOverview has no default grant and no policy of its own, because no endpoint is authorized by it; it is granted per project, to groups or to all members, and it never admits ProjectSearchReadPolicy. With the flag off, project details are as described above.

Adding an endpoint

  • Search data: see search reads; never ProjectViewPolicy alone.
  • Project or stage data: use the matching Project…Policy/Stage…Policy on a route with {projectId} (and {stageId}), and bind every body or route ID you touch to that project inside the action (404 if it is foreign, writing nothing). StudyController's study and correction actions show the pattern; Calculate Risk of Bias authorization shows a new activity end to end. A new activity needs its ProjectActivity/StageActivity member (appended, never inserted) and a ResourceSecurity.json default.
  • Application-wide administration: an Application…Policy whose activity is in the PM allowlist (application-authority mode).
  • Otherwise: add an EndpointAuthorizationExceptions entry. Use AuthenticatedSelf only when the action reads or writes nothing but the caller's own records. Use InActionAuthority when the action decides through an authority gate in its body. Use TrustedService for workload identities, never human sessions. Anonymous and PublicMetadata entries list their response fields; copy the "actual" list from the failure message only after checking that every field is fit for an anonymous or non-member caller.
dotnet test src/services/api/SyRF.API.Endpoint.Tests --filter "FullyQualifiedName~EndpointAuthorizationCatalogTests"