Skip to content

Application claim-consumer ledger (M2a)

Every place that reads the Identity application-role claim (https://claims.syrf.org.uk/syrf_groups, exposed as ICurrentUserService.GetSyrfAppGroups() in .NET and user.syrf_groups in the web app), or the application groups derived from it (EffectiveApplicationGroups), each with one owner. This ledger satisfies the M2a exit criterion of the implementation plan: it lists no consumer without an owner. Source was pinned at the M2a head of #3867.

Owner key:

  • M2a: converted by this milestone. With both application-authority flags on (enforced), PM authority decides and the claim grants nothing. Off and shadow behave exactly as before.
  • M1b: already PM-only.
  • M2b, M3a, M4b, M7a: the later milestone that owns the consumer, with the reason given in the row.

Line numbers are indicative; the file and symbol are authoritative.

API: application capabilities (HTTP, direct bearer and BFF sessions)

Each capability below requires the PM Administrator role (policy application-authority.m2a.1; since the M2b reader gate application-authority.m2b.1, which adds ImpersonateUsers → SupportImpersonator only). The API decides every request through the same gate (IApplicationAuthorityGate), so BFF sessions and direct bearer tokens get the same answer on every replica.

# Consumer Where Capability Owner Enforced behaviour
1 ApplicationListUsersPolicy: GET /api/investigators InvestigatorController.cs:31 through AuthorizationHandler ListUsers M1a PM decides; outage 503
2 ApplicationListEmailTemplatesPolicy: GET /api/admin-email/email-templates EmailAdminController.cs:108 ListEmailTemplates M2a PM decides; outage 503
3 ApplicationModifyEmailTemplatesPolicy: POST/PUT/DELETE /api/admin-email/email-templates… EmailAdminController.cs:125,145,165 ModifyEmailTemplates M2a PM decides; outage 503
4 ApplicationBatchAdminProjectsPolicy: POST /api/projects/update-all-study-inclusion ProjectController.cs:437 BatchAdminProjects M2a PM decides; outage 503
5 ApplicationBatchAdminProjectsPolicy: the statistics administration controllers (fleet, pilot-status, fence diagnostics, mode, delta, receipt and history maintenance, admin) ProjectStatistics{Fleet,PilotAdmin,FenceDiagnostics,ModeAdmin,DeltaMaintenance,ReceiptMaintenance,HistoryMaintenance,Admin}Controller.cs BatchAdminProjects M2a PM decides; outage 503
6 Parity read (ProjectStatisticsParityReadHandler): the human branch defers to BatchAdminProjectsPolicy ProjectStatisticsParityReadAuthorization.cs:112 BatchAdminProjects M2a PM decides. The outage reason is now carried out of the nested policy, so the answer is 503, not 403. The machine-client branch is unchanged.
7 Runtime feature-flag provider status: GET /api/runtime-feature-flags/provider-status (an inline administrator/SyrfAdmin claim check) RuntimeFeatureFlagsController.cs:46 ManageRuntimeFeatureFlags M2a PM decides; outage 503; SyrfAdmin confers nothing
8 Runtime feature-flag mutation: PUT /api/runtime-feature-flags/{key} and groups/{key} (the same inline check) RuntimeFeatureFlagsController.cs:84 ManageRuntimeFeatureFlags M2a PM decides; outage 503. The production restriction (MutationAllowed) still applies after authorization, unchanged in every mode.
9 Application-role administration and self-capabilities: GET/PUT /api/investigators/{id}/application-roles, GET /api/account/capabilities, POST …/pending-deactivation/reconcile InvestigatorController.cs, AccountController.cs:127 ManageApplicationRoles (self-capabilities: all seven) M1b PM only, enforced only (404 otherwise)
10 The application branch of AuthorizationHandler (the Application… requirement) AuthorizationHandler.cs:74 every row above M1a/M2a Every ApplicationAuthorization activity is in the PM allowlist. PM Core's EveryClaimPolicyApplicationActivityIsPmDecided test fails if a new application policy is added without one.
11 The application branch of SignalRAuthorizationHandler SignalRAuthorizationHandler.cs:52 any application requirement M2a Now uses the same gate as HTTP. No hub method carries an application policy today; the conversion stops a future one from reintroducing a claim grant.
12 ApplicationSendEmailPolicy (added by #3466; before it, no policy at all): POST /api/admin-email/send-templated-email, send-bulk-templated-email, send-templated-email-to-all-active-investigators EmailAdminController.cs:48,63,75 SendEmail M2a PM decides; outage 503. Off/shadow: the #3466 administrator claim check, which no mode removes.

API and shared libraries: claim consumers owned by later milestones

These read the claim or the derived groups, but not to grant an application capability. Converting them in M2a would change behaviour with the flags off, or belongs to another milestone's contract.

# Consumer Where Owner Why
13 Impersonation gate: syrf_groups must contain administrator UserImpersonationMiddleware.cs:58 M2b (done, #3869) With impersonationReadOnlyEnforcement on (it requires enforced authority) the claim gate is replaced by the actor's PM ImpersonateUsers (SupportImpersonator only), re-read on every request and hub invocation. Requests are decided on the target only: GetSyrfAppGroups() is empty while impersonating, so the actor's claim grants nothing. Read-only unless a valid edit context is presented; dual audit. Flag off: the legacy claim gate, exactly as before. How-to.
14 EffectiveApplicationGroups (claim groups ∪ Investigator role names) passed into project and stage permission evaluation AuthorizationHandler.cs:73 (project and stage branches), SignalRAuthorizationHandler.cs:50, EffectiveApplicationGroups.cs M3a (done in enforced mode, #3879) With authority enforced, the HTTP and SignalR project/stage branches decide through ProjectAuthorityEvaluator on linearizably read project facts; application groups and roles grant no project or stage content (A2) and active membership comes first (A1). Off/shadow: the union below is still evaluated, unchanged. The shipped ResourceSecurity.json grants no project or stage activity to an application group.
15 Project and stage re-evaluation inside endpoints (GetSyrfAppGroups() into project authorization, statistics shaping, review eligibility) ProjectController.cs:193,1125,1173, ReviewController.cs:424,789,1062,1247, StageStatisticsController.cs:74, StageStatisticsHistoryController.cs:41, ProjectStatisticsHistoryController.cs:39, ProjectOverviewStatisticsControllerBase.cs:42, ProjectStatisticsParityAdminController.cs:121, QuestionCountsController.cs:41, StageReviewSettingsController.cs:49,161, ReviewerScreeningHistoryController.cs:44, ReviewerAnnotationHistoryController.cs:45, SearchController.cs:87, SubmitAnnotationSessionService.cs:183, ProjectStatisticsChangedConsumer.cs:34 (push-time View check; enforced: row 17), PermissionReportResolver.cs:32 M3a (report done, #3879; in-endpoint checks open) Enforced: PermissionReportResolver returns the evaluator's report, so the report and the handler decision come from one fact set. The in-endpoint re-evaluations still run the legacy model on the cached project after the handler has admitted the request; they can only narrow for an active member, and grant through an application group only if a permission names one (no writer does). Converting them is the M3a follow-up; ReviewController belongs to paused review-eligibility work (#3746) and waits for it.
16 PM Core and Mongo.Data stage evaluation StageReviewService.cs:166, ReviewEligibilityService.cs:53, StudyRepository.ActivityReviewWrites.cs:43,171, StudyRepository.ActivityReservations.cs:59, ProjectStatisticsAuthorizationContextSource.cs:80, ProjectScreeningAuthoritativeQuery.cs:34, MembershipScreeningVisibilityShaper.cs:54 (statistics shaping, via ProjectStatisticsCaller.ApplicationGroups) M3a (open) Stage policy, as row 15. Every caller is behind a handler stage policy that M3a decides when enforced. ReviewEligibilityService and the activity reservation/write paths belong to paused review-eligibility and proportional-allocation work (#3746, #3251) and are converted with them.
17 Realtime delivery with saved groups NotificationHub.cs:1218, SignalR/Statistics/ProjectStatisticsSubscriptionFilter.cs:24 M4b (done in enforced mode, #3889) Enforced: every protected push is decided at delivery by IRealtimeDeliveryAuthority on fresh subject and project facts (linearizable); the full-stats leaderboard tier comes from the same facts and the captured groups are not consulted. Off/shadow: the saved claim groups are still used, unchanged. How-to.
18 Machine-client recognition: the absence of syrf_groups (and UserId) marks a client-credentials token ProjectStatisticsParityReadAuthorization.cs:80 M7a This uses the claim's presence, not an authority decision. When M7a stops emitting the claim, UserId alone still distinguishes the two token kinds. Retire the reference in M7a.
19 Claim projection into the principal: BFF session DTO, session authentication and the E2E login BffAuthController.cs:327,462, SessionAuthenticationHandler.cs:235, UserSession.cs:27, E2EAuthController.cs:96,201, CurrentUserService.GetSyrfAppGroups M7a This feeds the off and shadow fallback and must stay until the claim is retired.
20 Legacy group configuration: ApplicationPermissions[].AllowedApplicationGroupNames: ["administrator"] ResourceSecurity.json M7a This is the claim decision used in off and shadow and compared in shadow. Remove it with the claim.
21 Identity claim emission and storage UserClaimsService.cs:45, AuthorizationController.cs:608,673, ApplicationUser.SyrfGroups M7a/M7b "Stop application role claim projection" (M7a) and field cleanup (M7b).
22 Identity migration tooling that writes or verifies SyrfGroups SyRF.Identity.Migration (UserImporter, InvestigatorReconciler, ReadinessCommand, VerifyCommand, CampaignFinalCommand) M7a (with #2466) These are M005 parity tools, updated through their owner, not silently contradicted (plan M7a).

Web: presentation and direct navigation

The web app never enforces anything; the API decides every request. Since M2a the web presents the backend capabilities response wherever PM authority decides. It falls back to the claim-group check only where the capabilities read answers 404 (off or shadow), has not answered yet, or failed. In those cases it behaves exactly as before M2a.

# Consumer Where Owner Now
23 selectIsSyrfAdminUserAuthenticated (auth.selectors.ts:187) core/auth/auth.selectors.ts M2a (fallback), M7a (removal) Used only as the fallback inside selectApplicationCapability, selectCanAdministerApplication and requireApplicationCapability.
24 ensureSyrfAdmin direct-navigation guard (admin.routes.ts:36): Statistics Pilot and Feature Flags admin/admin.routes.ts M2a Replaced by requireApplicationCapability('batchAdminProjects' / 'manageRuntimeFeatureFlags', 'claim-administrator'). Each entry re-reads the capabilities.
25 Unguarded admin routes: User Impersonation and Email Templates admin/admin.routes.ts M2a requireApplicationCapability('listUsers' / 'listEmailTemplates', 'unguarded'): guarded where PM decides, and exactly as before (unguarded) elsewhere. Impersonation itself moves to ImpersonateUsers in M2b.
26 Send Emails page admin/emails M2a (no change) A placeholder page with no API call and no data, so there is nothing to guard. Row 12 covers the send endpoints.
27 Application Roles page admin/application-roles M1b Unchanged. The page reads PM authority itself and explains a denial.
28 Nav Admin menu visibility and its entries (User Impersonation, Feature Flags, Update All Project Inclusion) core/nav/nav.component.{ts,html} M2a The menu shows when any capability is allowed. Each entry needs its own capability (listUsers, manageRuntimeFeatureFlags, batchAdminProjects). The debugger, GUID-decode and project-level tally items stay with the menu.
29 Admin UI Options toggle (selectShowAdminUiOptions), which shows debug JSON in stage review and gates selectShowUpdateInclusionInfo core/state/ui/admin/admin-ui.selectors.ts M2a Now showingAdminUiOptions && selectCanAdministerApplication. It presents only data the caller already received.
30 Admin investigator list loading (getInvestigators$, Auth0 only) core/services/admin/admin.effects.ts:43 M2a Waits for selectApplicationCapability('listUsers').
31 403 handling core/http-interceptors/capability-refresh-interceptor.service.ts M2a A 403 re-reads the capabilities, but only where PM is known to decide and no read is running. It never fires in off or shadow mode, and a refused capabilities read never triggers another.
32 Claim parsing into the user entity core/auth/auth.effects.ts:77,89, bff-auth.provider.ts:107, user.entity.ts:38 M7a This feeds the fallback. Remove it with the claim.

Not a consumer: create-project-group.component.ts:45 lists the project permission group name administrator, not an application role.

Totals: 32 rows. 15 are converted or verified in M2a (rows 2–8, 10–12, 24, 25, 28–31), and 1 is unchanged by design (row 26). The rest belong to M1a/M1b (1, 9, 27), M2b (13, done), M3a (14–16), M4b (17, done) and M7a/M7b (18–23, 32). No row is unowned.

Resolved during M2a: row 12

While building this ledger, M2a found that the three AdminEmailController send actions had no authorization beyond sign-in. Any signed-in user could send SES templated email to arbitrary addresses, or to every active investigator, and that response listed each active investigator's ID and email address. No web page called these actions. The flaw predated this programme. Gating it changes behaviour with the flags off, so it was reported to the controller as an M2a stop condition rather than changed here.

#3466 fixed it in every mode with an unflagged SendEmail activity requiring the administrator claim. M2a then added SendEmail to the PM allowlist, so in enforced mode the same Administrator requirement is decided from PM authority. Off and shadow keep the #3466 claim check exactly.