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.