Skip to content

Application authority transition: design for review

Outcome in plain English

SyRF decides what a person may do; Identity establishes who signed in. Keep administrator status in Investigator.ApplicationRoles, and project/stage permissions in PM records. Removing access must deny the next protected operation, including from an already-open session. Being a site administrator does not automatically grant protected project content access.

Deliver this incrementally: first prove a usable grant/revoke path with existing sessions and two API replicas; then converge every permission consumer, suspension and live/queued work; only then remove obsolete role copies. Harden the independently verified RabbitMQ boundary in a separate coordinated lane. The companion implementation plan defines slices, dependencies and rollout gates.

Three distinct propositions must not be collapsed:

  1. Independent greenfield recommendation: for the first-party Angular/C# application, backend sign-in, server-side session and secure cookie, with application-owned authorization; no separate issuer by default.
  2. Near-term implementation: retain developed Identity, BFF/native compatibility and stable IDs while correcting application authority. Preserve password recovery, Google linking, admission, security-generation and session controls. Do not recreate them or rename schemas just to resemble the independent proposal.
  3. Later topology decision: retain versus consolidate Identity after measuring lifecycle/worker compatibility, operations, security, effort and risk. Neither deletion nor permanent separate deployment is approved.

The external API discussion was hypothetical and its claim that both client types were required was withdrawn. Chris subsequently identified potential eventual external systems needing to authenticate as user accounts. That possibility belongs in the topology option assessment, not present implementation scope. Prefer secure user delegation over sharing passwords if it becomes real; client types, consent, flow and scope are unresolved. Do not remove issuer capability irreversibly before that decision, or claim the possibility mandates it today.

Contents

Status, ownership and evidence

Approved design; implementation under way. M0a (#3853), the P3 benchmark (#3856) and M1a (#3858, dark and default-off) are delivered; M1b's role grant/revoke and self-capabilities API (#3860, enforced mode only) implements the contracts below; M1c (#3865) applies its last-administrator guard to every lifecycle path (pending-deactivation reservation before Identity destruction), so with enforcement on no supported path removes the last active administrator; M2a (#3867) converges every remaining application capability, its direct API checks and the web presentation onto that decision (the claim-consumer ledger owns every consumer; browser state stays presentation only); the plan records each milestone's status. This document proposes implementation details; neither publication, green CI nor cloud review authorizes implementation, account changes, deployment or merge. The user must approve the plan. Unanswered batch recommendations remain proposals, not consent. Subsequent production access-impact and deployment approvals remain separate even after design approval.

This is the integration design for #3335, not a second permissions programme. Its existing WP2–WP4 evaluator/authority work, WP6–WP7 revocation/jobs, WP9 reporting and WP10 impersonation map to the companion milestones. Coordinate ownership before assigning executors; do not duplicate open work. Allocation/capacity and membership-schema delivery stay with #3264/#2470/#2042. No historical WIP is reset.

Authentication delivery remains under #2466 and its M005 roadmap and validation ledger. This plan does not open G1–G6, execute S30, replace their evidence ledger or infer provider cutover from dark Identity readiness. Its authorization acceptance is additional, not a replacement for authentication parity/campaign/recovery gates.

Read the as-built audit, independent proposal and critical comparison, and source/runtime inventory and verification. Source is pinned to 0026b56a80fe85f1aea0f690663b02663de6899e; verification at audit commit 72f70877e found identical application sources. Deployment-reported versions differ; rebase/reverify paths before implementation. The 222 passing mock/component tests do not prove real issuer, cross-replica or queued-work acceptance.

The 8 September #3335 handover remains accepted-decision context, not a fresh census. In particular this design does not adopt its earlier suggestions that a two-second cache meets R1, that turning a flag off may restore claim grants after revocation, or that old commands without an actor may proceed merely with a log entry. Those mechanisms need the stricter contracts below. Do not reuse historical zero-population counts to delete data.

Authority and decision contract

Concern Target contract
Identity Credentials, provider links, assurance, stable account-to-Investigator mapping, native tokens/session validity; no application-role authority.
Application roles Existing PM Investigator.ApplicationRoles; canonical ApplicationRole value-object name Administrator, explicit approved import mappings only. Unknown source values stop migration, never auto-map.
Project/stage Existing PM project memberships/groups/grants; active membership before protected-content grants; retain owner permission-assignment authority, additive groups and all-active-members grants. No new group-deny model.
Machine authority Named workload plus bounded resource/workflow capability. Never infer human Administrator from broker access or client-credentials authentication.
Decision One evaluator produces allow/deny, reason, scope and policy version from validated subject + current facts. HTTP, SignalR, reports and jobs share it. UI is explanatory, not an enforcement boundary.
Impersonation Real actor must hold explicit ImpersonateUsers (the capability name and S1–S7 semantics are defined in #3335, the definition of record); effective target permissions only; no privilege union. Server-enforced read-only default and explicit edit mode, dual attribution, reset/unsaved-change semantics from #3335 S1–S7.

Proposed storage for that explicit support grant is an additive SupportImpersonator value in the existing ApplicationRoles field/value-object parser, mapped only to ImpersonateUsers; it confers neither Administrator nor project membership. Confirm BSON/parser compatibility before adding it and approve its holder list separately. This is a proposed implementation choice, not an already accepted role or a new general-purpose grant store.

Proposed shared pure evaluator: IAuthorizationEvaluator in SyRF.ProjectManagement.Core/Authorization/. An IAuthoritySnapshotReader loads facts; an API/worker operation service coordinates authentication, authorization, feature availability, allocation/capacity and effects. Do not put I/O or feature flags in the pure evaluator, or make capacity reservation confer permission. Return 401 for missing authentication, 403 for known denial, existing resource-concealing 404 where appropriate, and 503 for unavailable authority; no side effect or protected payload on an unavailable decision. Reason details for other users require their own administrator capability; ordinary users receive only safe self-access explanations.

The minimal first slice adds a guarded role mutation and self-capability read to the API, a small administrator grant/revoke UI and ListUsers enforcement. Proposed contracts: GET /api/account/capabilities, PUT /api/investigators/{id}/application-roles with expected revision and reason. Server resolves the real actor, validates allowed roles and target mapping, rejects impersonated role administration, and writes an idempotent, majority-acknowledged change with audit attribution. 409 means revision conflict; retry requires fresh state, not blind replay. Never let a caller supply authority-bearing actor/groups in the body.

The role manager is itself protected by current PM authority. Bootstrap requires a separately approved, operator-run idempotent import of the approved initial administrators; no anonymous first-admin path or claim-driven auto-bootstrap. Prevent last-active-administrator removal through serialized role-administration transactions with a shared revision guard, not a race-prone count-then-save. Every supported transition that makes an administrator inactive participates in that same guard: role removal, application suspension, account deletion and administrative deactivation/import paths. Account deletion must reserve/check the guard before destructive Identity deletion; checking only before the later PM save is too late. Direct database edits remain unsupported outside an explicitly approved operator recovery procedure.

Cross-service deletion uses a durable pending-deactivation reservation in the guarded PM transaction, counting that administrator as unavailable when admitting another removal. Then call Identity outside the transaction with a server-bound operation ID. The same transaction persists a pending-deactivation marker on the Investigator; the authority reader treats it as a denial input until reconciled, including for still-valid legacy tokens. This prevents the reservation being merely an administrator count rather than an access fence. After the Identity result, finalize PM state idempotently. An ambiguous deletion outcome retains the reservation/denial until reconciled; release it only after proving no destruction occurred. Never hold a Mongo transaction across the HTTP call or automatically restore eligibility after an unknown result. Suspension and role edits serialize against these reservations as well. Concurrent deletion, suspension, role-removal and administrative deactivation/import tests must prove at least one eligible administrator remains. Exercise every path against another removal and an existing pending-deactivation reservation, including partial failure, retry and reconciliation cases. This small coordination record is justified by the invariant, not a replacement role schema. Recovery of an already-zero-admin state is an explicitly approved operator procedure, not a hidden application bypass.

The first slice's scope is named capabilities only. It must not advertise global role revocation until the remaining privileged routes/impersonation migrate. Production activation of global authority requires the M2–M5 coverage ledger; intermediate code can ship dark and the vertical feature is usable in isolated/staging scope.

Freshness, concurrency and failure

R1 boundary: a protected operation beginning after a successful majority-acknowledged revocation must observe denial on every replica. No two-second or refresh grace. Checks for saved annotations, exports and delivery are not satisfied merely because admission once succeeded. Existing writes begun before revocation are in-flight: recheck before effects where feasible; do not promise retroactive cancellation or retract sent bytes. The operation-specific boundary is documented and tested, not silently converted into a final-save exception.

Proposed correctness-first reader bypasses RepositoryCache and reads each security-bearing document by immutable _id on the primary with linearizable read concern and bounded maxTimeMS (initial recommendation 5 seconds); mutations use majority acknowledgement. Validate support on the actual Mongo topology/driver in M0 (done: P3 benchmark, PASS on 2026-09-30). This is a dedicated authorization read path, not a blanket change to all repositories. No positive permission cache or saved role snapshot authorizes an operation. Failure/timeout denies execution with retryable 503.

MongoDB's linearizable-read contract is single-document, primary-only and not available with causally consistent sessions; it can be slower and needs a timeout. Do not claim these separate reads form a multi-document transaction. Grant edits spanning documents must define ordering, use supported transactions/fences as appropriate and test concurrent withdrawal. For R1's post-commit start boundary, each later authoritative read must observe completed changes; for in-flight races, the operation contract controls the final-effect check. If this reader is unsupported or too costly, return to design review with a proven revision/fencing alternative; do not silently substitute cached or majority-only reads.

Permission writes include persisted audit evidence atomically with the mutation; an outbox/event can refresh UI or release reservations after commit. Notification delivery is not the revocation security boundary: every protected operation/delivery still reads authority. Record decision latency/failure aggregates and policy revision without subjects, tokens or project content. This follows the per-request checks/deny-default principles in OWASP authorization guidance.

Lifecycle, browser and workers

  • Browser: retain BFF/native and necessary legacy-provider compatibility initially. Capabilities come from the API and refresh on access-change notifications, 403 and re-entry; stale UI never grants access. Preserve drafts locally only where existing privacy contracts permit; no unauthorized last save. Ordinary cookie security/CSRF and stable mapping remain intact; backend access tokens stay server-side in BFF mode.
  • Direct tokens: authenticate identity/audience normally, then apply identical PM checks. Old role claims must be ignored once their capability's authority cutover is active, even on otherwise valid tokens.
  • Suspension: proposed reversible application suspension uses PM Deactivated as the deny input only after confirming existing deletion/restore semantics. Separate suspension provenance/audit from destructive deletion; a deleted or missing identity is not restorable. Deny application actions first; perform idempotent native token/session withdrawal as a separate tracked outcome. Restoration never clears unrelated lockouts, recovery requirements or revoked grants; fresh authentication is required if sessions were withdrawn. M4a confirmation: Deactivated turned out to be deletion-only provenance (one writer, never cleared), so reusing it could not keep suspension distinguishable from deletion. Suspension therefore denies through its own ApplicationSuspension field, which every authority read treats exactly like Deactivated (see P7).
  • SignalR: authorize subscription, hub invocation and protected delivery with current actor/target and resource facts. Remove access to streams and release relevant capacity; failed notifications cannot grant. Do not let already-running queries publish from stale context after the controlled revocation boundary.
  • Jobs: new user-delegated work requires server-bound initiator, actor, action/resource and persisted job identity; recheck on execution/retry. Admitted consistency/cleanup obligations use explicit system authority. Committed events remain facts but recipients are checked now. Legacy missing-actor jobs are quarantined or classified from trusted persisted admission records, never blessed by a correlation ID. No blanket JWT forwarding.

Public project discovery/RequestToJoin must be catalogued separately from protected studies, PDFs, annotations, exports and membership details. Existing ProjectPermission application shortcuts are configurable, not proof of a universal admin bypass. Classify exact response fields before retaining a public exception; default new content routes to protected. Keep separate batch RoB; retire only the MapsGroup prototype through #3762.

Migration and rollout design

Expand before contract: compatible readers, isolated proof, approved mapping, shadow comparison, coordinated enforcement, then legacy consumer/storage cleanup. Shadow may compare decisions but never add a grant or collect personal data in public evidence. No periodic Identity→PM role synchronization or reverse dual writer. Import only reviewed values once; subsequent writes are PM-owned. Persist source hash/mapping version, expected target revision, before/after and operation ID in a restricted migration ledger. CAS conflicts/unknown values stop the affected record. Do not overwrite later role edits on retry or restore an old backup over revocations.

Proposed schema-generated flags: reuse/define syrfOwnedApplicationRoles and per-slice enforcement controls only after checking the current catalog. Separate off/shadow/enforced deployment modes with no contradictory per-pod overrides; production remains GitOps-only. Upgrade every reachable API/worker before enforcement; route/drain old replicas and reject mismatched policy revisions. No mixed fleet may authorize the migrated capability under old claims. A persisted monotonic authority-version marker prevents an old binary/flag rollback from silently reopening access; incompatible instances fail readiness/deny protected operations. The marker only protects binaries that implement its check. M6 must also restrict deployment/rollback to a reviewed compatible-image allowlist and drain old replicas; a marker cannot make an old binary obey new semantics. Verify that traffic/deployment gate separately before claiming fleet-wide R1.

Rollback before any authority mutation may disable shadow. After activation, retain the denial guard and new PM state; revert UI/optional logic only to a compatible release, or freeze affected operations. A rollback that re-enables stale claims is prohibited. Test recovery from an interrupted import, failed notification, partial native withdrawal, unavailable authority and mixed-version fleet. No schema/claim deletion until readers, external/internal client compatibility and the approved rollback window are demonstrated.

Decisions for approval

Accepted inputs: C1/C2 application ownership + explicit access-impact confirmation; R1; A1/A2/A3; existing group semantics and #3335 impersonation S1–S7. Chris has selected the existing Investigator role field and retention of Identity for the near-term correction. None approves an import population or production rollout.

For reviewability, the accepted 8 September #3335 membership/ownership decisions mean:

  • A1: active, enabled project membership is required for ordinary protected project/stage operations, including group and individual grants. Disabled membership suspends work; historical contributions remain. An exception must be explicitly designed/configured, never emerge accidentally from grant composition.
  • A2: a system-administrator role alone grants no protected project content without active membership. Any support exception must be explicit and audited; no new exception is enabled by this plan.
  • A3: an active project owner retains project-scoped permission-assignment authority, including assigning permissions to themselves, but ownership is not a universal grant for ordinary actions. It grants no application-role administration or bypass of feature/allocation/capacity rules. Preserve the existing guard requiring ownership transfer before disabling the owner; do not invent an implicit recovery exception.

These are summarized accepted product rules, not proof of current deployment; exact public metadata exceptions remain classified separately under P8. They do not assume old census results remain true.

Decisions recorded 2026-09-30 by Chris: approved as written: P2, P4, P5, P6, P8. P1/P1b: approach approved; holder/source mapping approved separately through M0b, never inferred. P3: conditional on the M0 benchmark proving linearizable reads, deadline and failover acceptable; accepted 2026-09-30 (controller) under P5 after the benchmark passed. P7, P9, P10: deferred until their milestones. M0a and M0b are authorised; M1a was authorised on 2026-09-30 and is implemented dark (#3858); M1b was authorised on 2026-09-30 (relayed by the controller) and its API is implemented dark and enforced-mode-only (#3860), with its Admin Console editor (#3861) and the operator bootstrap (#3862, not run against any real environment) as follow-on M1b PRs. M1c was authorised on 2026-09-30 (relayed by the controller) and is implemented dark (#3865). M2a was authorised on 2026-09-30 (relayed by the controller) and is implemented dark (#3867). M2b was authorised on 2026-10-01 (relayed by the controller); its reader gate (#3868) adds SupportImpersonator → ImpersonateUsers only, with no holder; the server contract (read-only default, 30-minute edit contexts, dual audit, target-only authority with no escalation) is implemented dark behind impersonationReadOnlyEnforcement (#3869). M3 was authorised on 2026-10-01 (relayed by the controller); M3a puts project/stage policy and the permission report on one evaluator and fact set, dark and enforced-mode only (#3879). M4 was authorised on 2026-10-01 (relayed by the controller); M4a implements reversible application suspension dark behind applicationSuspension (#3888) and M4b enforces current authority on realtime delivery, dark (#3889). M5a was authorised with the programme (relayed by the controller) and binds delegated jobs to persisted authority, dark (#3894). M5b was authorised on 2026-10-02 with Chris's P9 decision (relayed by the controller). M6 or later code, SupportImpersonator holders, environment flag changes, production, broker or account changes are not authorised and each needs separate approval.

Proposed default Approval item / alternative Decision (2026-09-30, Chris)
P1 existing role field; canonical Administrator; explicit import Approve exact source mappings and holder list separately; never infer from historical census. Approach approved; holder/source mapping approved separately via M0b, never inferred.
P1b SupportImpersonator in the same field for the accepted support capability Approve the additive role representation/holders; alternative explicit capability storage needs a revised migration contract. Approach approved; holders approved separately via M0b.
P2 first grant/revoke + ListUsers capability slice Approve slice boundary; another capability would change UI/tests but not authority contract. Approved as written.
P3 uncached linearizable authority reads, 5-second deadline Confirm correctness-first tradeoff after M0 benchmark; revision-fenced cache requires a new proof, not R1 relaxation. Conditional on the M0 benchmark proving linearizable reads, deadline and failover acceptable. Benchmark 2026-09-30: PASS (results). Accepted 2026-09-30 (controller) under P5: the ~10–12 s fail-closed window on an unplanned primary kill/partition is P5's accepted temporary unavailability; Atlas planned maintenance uses graceful step-down (0 failures).
P4 shadow then coherent environment activation No per-user bypass or mixed-version enforcement. Approved as written.
P5 deny-preserving rollback Accept temporary unavailability over silently restoring revoked access. Approved as written.
P6 delayed destructive role-copy cleanup Keep ignored fields until compatibility/rollback proof; no dual-writer synchronization. Approved as written.
P7 reversible PM suspension + separate native withdrawal Confirm state/provenance and restoration boundary before M4; preserve independent lockouts. Deferred until M4. Confirmed for M4a (#3888), dark: Deactivated is deletion-only provenance (one writer, never cleared), so suspension is its own ApplicationSuspension field. Restore removes only that field, never for a deleted account, never touching Identity, roles, lockouts or recovery; withdrawn sessions need a new sign-in.
P8 public metadata only; no new support exception Exact field/route classification approved with #3335; retained impersonation still migrates. Approved as written.
P9 job-family admission classification Job owners approve durable-obligation exceptions; absent actor does not imply authority. Deferred until M5. Decided by Chris 2026-10-02 (owner: Chris): data-changing jobs (reference import, bulk study update, batch RoB) are atomic and roll back when stopped by a permission change, cancellation or failure; tidy-up and consistency jobs (screening inclusion recalculation, bulk-PDF clean-up, the bulk-update rollback) finish as system obligations; legacy bulk updates are classified from StartedByInvestigatorId. Per family: queued-work ledger.
P10 separate broker credential/ACL/TLS transition Infrastructure owner approves rollout and recovery plan before any live changes. Deferred until B0–B2. B0 proposal for review: broker security contract (no live change).

The plan is executable against these explicit defaults once approved; unknown populations, client usage and deployment alignment are acceptance gates with owners, not guessed facts. No further product interrogation is required before reviewing this pair. Topology consolidation remains a later decision, including the possible future user-account integration and its option cost. Optional history-reader UI, new support-access mechanisms, cache optimization and broad group/schema redesign stay outside the first slice.