Skip to content

Service and client inventory in the overall authorization model

Outcome and evidence boundary

SyRF has independent asynchronous business workflows, not just Identity-related HTTP calls. API and Project Management (PM) authenticate as workloads to RabbitMQ; the API separately authenticates people and evaluates application permissions. Neither multiple pods nor a queue requires forwarding a browser JWT. A machine credential authenticates the worker, not the person whose project it affects.

This extends the initially targeted architecture comparison with a systematic repository scan on 27 September 2026. It does not establish a complete live traffic inventory or approve removing OAuth. The as-built authority audit remains the reference for Identity/PM roles, mappings, BFF claims and freshness.

Initial 27 September search scope and reproducibility

Source revision: 0026b56a80fe85f1aea0f690663b02663de6899e. The scan covered all src/services and src/libs, including API, PM, Quartz, Identity endpoint/migration/shared code, PDF agent, S3 notifier, Angular and the Python helpdesk Lambda. Tests/generated clients were separated from production callers. Search families included:

  • HTTP clients and request factories, REST/HTTP helpers, SDK clients, gRPC/protobuf, WebSocket/SignalR, browser fetch/XHR, API keys, authentication schemes and OAuth grants.
  • MassTransit consumers/sagas, publishers, sends and request/reply; Quartz schedules, hosted/background services, job contracts and their composition roots.
  • AWS S3/SES/DynamoDB/SQS/Secrets Manager, Google Sheets, telemetry, database/cache clients and residual SSH/FTP references; authentication and integration documentation plus chart configuration names.
  • Development/e2e/migration scripts and client registrations, distinguishing test tooling from supported native/CLI/third-party API products. No gRPC service/client implementation was found; one commented instrumentation suggestion is not an integration. No executable SSH/FTP client use was found by the scan.

Representative matches were traced to caller, registration and receiving handler, rather than counting every HttpClient mention as an active integration. No external requests, database queries, secret reads, cluster queries or workloads were executed. This does not inspect dynamically loaded code, other repositories' client implementations, historical user usage or every effective production override.

A clean local GitOps checkout was inspected read-only at 5620f968365b9d3040aab014aad9af05f0fbd144. Only tracked service declarations, safe booleans/provider selections and credential reference structure were used. Secret resources and RabbitMQ's secret-backed imported definitions were not read. No fetch, render, sync or deployment occurred. This is a second pinned repository snapshot, not proof of applied state.

Status vocabulary

  • Declared: source is wired and a host is declared enabled in the inspected GitOps snapshot. This is desired-state evidence, not observed active traffic or proof every feature in that host is enabled.
  • Gated: implementation/activation controls exist; explicit configuration is needed. Where GitOps declares an activation, say so without inferring runtime acceptance.
  • Unknown use: executable source or publisher exists, but no adequate operational/owner evidence.
  • Mothballed: Chris explicitly excluded it on 27 September; code presence cannot override that decision.
  • Design-internal: created by the selected Identity architecture, not an independent business requirement.

No row below is promoted to “observed active” merely because a service has a deployment declaration. The inventory below preserves the initial source/configuration evidence; the dated verification follow-up separates subsequent runtime observations and isolated tests from that baseline.

Inventory by workflow and authority

For every row, browser cookies and application-role token claims remain separate decisions. “Machine” means a service identity or narrowly scoped capability, not a human administrator grant. A user initiating work can still need application authorization even when the subsequent transport uses only machine identity.

Workflow / status Caller → receiver and evidence Current mechanism and user-delegation implications
Reference-library import — declared hosts; workflow use unconfirmed API signs upload and emits start; browser uploads to S3; Lambda emits saved event; PM saga starts parser job. SearchController.cs:133, StartParseJobsActivity.cs:38 API project policy at admission; S3 signature/IAM; RabbitMQ service credentials. Message carries project/search metadata, not a user OAuth token. Define delayed-work authority separately from the person's browser session.
Bulk-study update — declared hosts; use unconfirmed S3 notifier submits update job; PM validates job execution version and processes file. StudyUpdateJobFileSavedToS3Comsumer.cs:16 S3/broker identities plus PM job state. Not evidence for a delegated OAuth user token. Legacy version rejection and replay safeguards are domain checks, not broker permissions.
Screening settings and inclusion recalculation — declared hosts API project-edit operation emits command to PM. ScreeningController.cs:29, ProjectAgreementThresholdUpdatedHandler.cs:19 API actor from authenticated current-user service; command includes UpdatedBy. Broker credentials carry the command; PM checks workflow state, not a forwarded JWT. Detailed trace below.
Idle/suspended review sessions and connection liveness — declared PM/Quartz hosts Review API schedules commands; PM idle/liveness/removal consumers process them. ReviewController.cs:345, RemoveIdleSessionConsumer.cs:31 RabbitMQ scheduler/worker identity. These are review-workflow sessions, not Identity login sessions; do not confuse cleanup with user logout or global account revocation.
Quartz job/saga scheduling — declared Quartz hosts MassTransit scheduler and job sagas with SQL persistence. QuartzServiceCollectionExtensions.cs:67, :135–145 Broker username/password and database connection credentials. No inherent user-token or SyRF OAuth dependency. Scheduled command authorization remains the application's responsibility.
Bulk-PDF processing — gated; staging declaration present Browser multipart → S3 capture → RabbitMQ → ARRNC PDF agent → PM claim/progress/finalize. ProcessBulkPdfUploadConsumer.cs:27; hosting contract API authorizes upload; worker uses broker identity and claim-issued presigned download capability, not AWS credentials or human roles. Local clamd/storage gates are separate runtime trust boundaries.
Bulk-PDF cleanup/release reconciliation — gated; staging authority declared Scheduled Lambda → API notifier routes → PM command/reply; DynamoDB/S3/SQS reconciliation. BulkPdfScheduledReconciler.cs:146, :284–310 Narrow client-credentials bearer scopes plus environment/cleanup-authority binding; AWS workload credentials for cloud resources. Independent business need for machine authority, but OAuth issuer is an implementation choice. No human delegation.
Daily statistics, multipart sweep and receipt drain — gated PM hosted services schedule bounded jobs. DailyProjectStatisticsSchedule.cs:27; bulk-PDF activation contract Host/store/broker/AWS credentials as applicable. Background scheduling does not justify site-admin claims; execution must enforce persisted workflow/tenant boundaries. Per-feature deployed use not established.
Project updates and statistics/claim-revocation notifications — mixed: declared base streams, gated consumers PM writes domain state; API observes Mongo changes. Statistics outbox dispatch also fans out over RabbitMQ to API instances. AggregateRootEntitySubscriptionManager.cs:45, ProjectStatisticsDispatchService.cs:17 Mongo/broker machine identities; SignalR recipient uses cookie or bearer authentication. Current recipient permission is a separate disclosure check. Not every PM→API update is a bus event.
Google Sheets informational content — unknown actual use; executable API routes Anonymous API protocols/libraries/FAQs call Sheets service. ApplicationController.cs:99, SheetsServiceFactory.cs:28 Google service-account credential via SDK, not end-user Google OAuth delegation and not SyRF-issued OAuth. Credential file was not opened. Confirm continued business use before treating this as a constraint.
Application emails; Identity recovery/campaign emails — declared API; provider-gated Identity/campaign API SES client and selectable nonproduction SMTP; Identity outbox worker; explicit migration campaign. Program.cs:290; transport contract, recovery outbox AWS credentials or SMTP transport settings. Mixed business and authentication purpose, but transport is machine-to-provider, not delegated user OAuth. Campaign CLI is operational tooling, not evidence of a public CLI API client.
Helpdesk mail→GitHub issue/comment Lambda — unknown deployment/use Python handler reads mail/attachments/state and calls GitHub. lambda_function.py:20, :48–79,173–196 AWS workload identity plus GitHub App JWT→installation token. Tokens are GitHub's integration protocol, not SyRF user/role tokens. No Lambda invocation or secret retrieval performed.
Browser API, SignalR, S3 upload and PDF delivery — declared web/API; provider conditional Angular generated API client, auth adapter, SignalR and file services. signal-r.service.ts:575, s3-file.service.ts:94 BFF cookies or provider bearer depending mode; signed S3 upload/download capabilities and separately served PDF URLs. Selecting API cookies does not redesign object-storage or PDF access.
Swagger / developer / e2e clients — defined tooling, use unconfirmed Public code+PKCE Swagger registration; guarded test setup and synthetic harnesses. OpenIddictClientSeeder.cs:359 Swagger is user authentication; test credentials are harness-specific. Neither proves a supported public third-party/native/mobile client product. Such usage needs an owner/registry census.
BFF login/introspection, API Identity management, Identity PM provisioning, Google external login — design-internal/provider conditional As-built audit traces endpoints, stable mapping and account lifecycle User OIDC login, server-held tokens and separate machine grants. Preserve compatibility while evaluating replacement, but these architecture-created calls cannot circularly prove a need for a separate issuer.
Living-search notifications — unknown downstream use LivingSearchHandler.cs:20 publishes enable/disable events; no corresponding consumer found in this repository scan Broker service identity. Queue/config names for literature/PubMed/bioRxiv and other workers are not proof those external workers still run or require user delegation. Confirm with owners.
Risk-of-bias AI prototype and API-key callback — mothballed Maps/RobAi outbound call and RobAiController.cs:30 API-key response route Chris's 27 September clarification excludes this prototype from active/planned requirements; cleanup is separate PR #3762. The only explicit ApiKey controller-scheme use found belongs here; do not present it as an active public API or conflate it with batch RoB below.
Separate batch risk-of-bias processing — source-defined; actual use unverified SearchController.cs:232 persists a job then submits IStartRobProcessingJobCommand; RoBProcessingJobConsumer.cs:15 runs the processing service Broker workload identity; RobProcessingService.cs:59 orchestrates conversion, CSV/script work and result persistence. This is not the MapsGroup callback workflow. Preserve it; confirm owner and usage separately. No batch was submitted or research data read.
Mongo, Redis/Valkey, SQL, S3 and observability — declared/configuration-dependent infrastructure Mongo repositories; BFF Redis; Quartz SQL; S3 SDK; Sentry/OTLP exporters. SyrfConfigureServices.cs:84 Store/workload credentials, telemetry configuration and infrastructure access policy. Not browser delegation. Database/cache/network trust remains necessary with either cookies or OAuth.

Commented RequestService.cs HTTP/token examples are not runtime callers. Dependency packages, queue-name options and telemetry instrumentation were not counted as business clients without a call path. Documentation/build publishing and CI credentials are operational clients outside the end-user auth model; this audit did not turn a search of runtime requirements into a CI credential audit.

Export is a useful counterexample to inferring a worker from a job name. DataExportController.cs:22 applies ProjectExportDataPolicy, creates a persisted job with the current actor (:68), checks the job's project on download (:82), and streams the generated file from the API (:107). Its batched progress reporter uses a local task; the inspected route is not a separate OAuth-authenticated export service. Mid-stream revocation remains a distinct contract from admission and cannot retract bytes already sent.

How the layers fit: representative screening-settings workflow

The following is as-built source, not a newly implemented target. The existing browser mode determines cookie versus bearer; it does not determine RabbitMQ's credential.

flowchart TD
    H[Human signs in: Identity or configured provider] --> B[Browser session or bearer principal]
    B --> A[API authentication and stable Investigator mapping]
    A --> P[ProjectEdit policy reads PM Investigator and Project]
    P --> W[Save screening mode; actor from current-user service]
    W --> C[Domain handler sends recalculation command with UpdatedBy]
    C --> Q[RabbitMQ authenticates API workload and authorizes publish]
    Q --> M[PM workload consumes; validates pending project calculation]
    M --> D[PM updates studies and saves completed project state]
    D --> N[Mongo committed-change notification observed by API]
    N --> R[Existing SignalR subscription and recipient checks]
    R --> U[Browser receives project update]
  1. Person boundary: native sign-in/BFF introspection authenticates an account; the mapping identifies the Investigator. The authority audit documents claim snapshot and freshness limitations; successful authentication is not project permission.
  2. API resource boundary: ScreeningController.cs:29 applies ProjectEditPolicy. Its current authorization handler loads PM state and combines claim groups with PM roles. This is an actual check, not proof of the proposed single-authority/immediate-removal target.
  3. Actor provenance: that controller supplies CurrentUserService.GetUserId(), not a posted actor ID, to UpdateAgreementMode. The domain event feeds the handler's command ProjectId, threshold and UpdatedBy. The payload has no credential or current authorization proof. A correlation ID is for relating operations, not authenticating a user.
  4. Workload boundary: MassTransitHelpers.cs:47 configures RabbitMQ username/password. Queue routing follows message conventions; configuration can create/bind endpoints. Broker permissions decide whether the workload may publish/consume/configure. They do not inspect SyRF project membership.
  5. PM execution boundary: UpdateStudyScreeningStatsConsumer.cs:44 reads the project and requires a pending calculation, applies updates and closes it. UpdatedBy is used in logging; no fresh actor permission check or user-token validation was found in this consumer. Idempotent/fenced retry logic protects workflow consistency where enabled; that is not user authentication.
  6. Commit/publication boundary: ordinary MongoUnitOfWorkBase.cs:272 saves then dispatches domain events. This is not a crash-durable atomic Mongo+broker transaction. Do not promise exactly-once publication or general post-commit transactional semantics for every caller.
  7. Return/disclosure boundary: PM completion changes the Project. API AggregateRootEntitySubscriptionManager.cs:45 subscribes to repository notifications backed by Mongo change streams, not a fictional direct PM event. NotificationHub.cs:1070 checks ProjectView at subscription and handles disabled-membership changes during updates. This path is not proven to perform the complete current Investigator/group check before every delivery; do not generalize a stronger guarantee from it.

A second, explicit message-return path exists for statistics: committed invalidation state is read by the API outbox dispatcher, published by ProjectStatisticsNotificationSink.cs:18, and delivered through a separate temporary queue per API instance. ProjectStatisticsChangedConsumer.cs:24 reloads the Project and Investigator uncached, checks deactivation, disabled membership and ProjectView before sending. It still combines saved subscription claim groups with PM roles, so fresh reads do not eliminate stale claim grants. This is persisted-state→API-publisher→broker→API-consumer, not evidence that PM directly publishes every update. Keep the actual topology visible.

Initial RabbitMQ identity and permission evidence

The shared helper uses password-based broker authentication; no user bearer propagation middleware was found in the inspected MassTransit registration/handlers/contracts. The traced commands contain actor or resource IDs, not OAuth tokens. This is a bounded source finding, not a packet capture of all headers.

The API, PM and Quartz chart defaults reference the same broker username and password Secret name: values.yaml:79, values.yaml:85, values.yaml:39. The shared template reads the password via a Secret reference, not a user session: _env-blocks.tpl:225. The inspected GitOps environment values provide broker host/port/vhost, with no per-service RabbitMQ override in the API/PM/Quartz service and environment overlays checked. This establishes shared credential defaults/references, not distinct per-service principals. A config-map override, parent values or an applied deployment may still differ; effective workload identities, live credentials, broker permissions, TLS enforcement and imported definitions remained unverified in that source/configuration pass. The dated runtime follow-up below supersedes the ACL/listener unknowns for its observed broker only.

Queue definitions and temporary fan-out queues are visible in code; the effective broker's configure/write/read permission regexes were not established in the initial pass because definitions are secret-backed. Do not claim least privilege from queue names or environment vhosts alone. A later read-only broker/owner review should establish actual per-service identities, vhost isolation, publisher restrictions, TLS, rotation and the ability to forge commands from another workload. No secret values are needed in its report.

RabbitMQ's access-control documentation distinguishes connection identity, vhost admission and resource configure/write/read permissions; OAuth is one available mechanism, not mandatory. MassTransit RabbitMQ configuration describes transport credentials and endpoint topology, while its serialization documentation describes message/envelope metadata. Our application-level conclusion is that message IDs, headers and actor fields do not automatically prove end-user authorization.

Target contracts and the command/event distinction

These are proposed review criteria, not claims of implemented controls:

  • New deferred actions on behalf of a person: establish trusted actor provenance at ingress, bind the intended resource/action and persist a server-owned job record. Decide whether execution rechecks current actor permissions or uses an explicitly granted bounded capability. Never let an arbitrary publisher claim another actor merely by changing UpdatedBy.
  • Completion of an admitted durable obligation: cleanup, consistency repair or finishing a committed state transition may remain necessary after the initiator loses access. Its authority is the owned workflow/state machine and narrowly authorized worker, not the continued existence of that user's login. The screening recalculation needs this explicit classification; absence of an actor recheck alone is not enough to label it a bypass.
  • Events recording committed facts: verify trusted producer and event/state consistency; process retries idempotently. Do not discard historical facts just because the original actor has since lost permission. An event named “started” that triggers new work still needs a defined admission contract.
  • Fresh disclosures and subsequent user actions: independently check the current recipient's access before protected delivery/download/action. Historical event legitimacy does not authorize every listener.
  • Threat and ordering: a compromised or over-permitted broker principal can publish forged actor/resource fields or replay messages. Separate workload grants, persisted operation/version checks and idempotency address different parts of this risk. No protocol requires solving it by forwarding a long-lived user JWT. Authority failure must not fall back to stale allow; define revocation and in-flight behavior per workflow.

The reference import path is an additional gap example: its API entry has ProjectImportSearchPolicy, but the start event and parse command shown above lack an actor identity. PM validates saga metadata/job state, not a carried user credential. Before promising revocation of queued imports, specify whether they are revocable future user work or already-admitted durable imports, then test that contract. Do not invent an actor from a correlation/search ID.

Declarative deployment evidence and remaining decisions

The pinned GitOps snapshot declares production API, PM, Quartz, web and S3 notifier enabled; production PDF-agent configuration is disabled. Staging API explicitly selects Auth0 with BFF off, while bulk-PDF upload and notifier authority are declared on. Staging also declares Identity and a PDF service. These facts qualify older source-rule “feature-off/activation pending” descriptions, but prove neither live activation nor the ARRNC worker's state. A PDF static-server service declaration is not proof a processing worker is running.

Safe evidence: production service declarations, staging API values:78, :90,148,156, staging PM values:110. No recorded zero-usage proof counts were revalidated or treated as live facts.

What this adds to the architecture decision

  • There are more independent workflows than bulk-PDF cleanup: imports, study updates, scheduler-driven work, project notifications and email. Their existing trust mechanisms are mostly broker, storage or provider credentials—not SyRF user OAuth. They defeat a literal “single process/no integrations” assumption, not the cookie-session browser recommendation.
  • Bulk-PDF cleanup is a concrete SyRF bearer machine client; that makes removing its current issuer a compatibility task. It does not establish that OAuth is the only safe replacement or that humans need application-role claims in tokens.
  • Google Sheets and helpdesk are identifiable independent source paths, but usage must be confirmed. Living-search external consumers are unknown. The mothballed AI prototype and callback are excluded.
  • No supported external native/mobile/third-party delegated client population was established. Do not convert that search result into a claim that none exists. Swagger and e2e clients are separate categories.
  • Shared broker identity and inconsistent actor/freshness contracts deserve explicit review alongside application permission ownership. A browser authentication rewrite by itself would not repair them.

Before choosing the long-term topology, obtain an owner-confirmed workflow/client census; verify broker principal/ACL composition and actual deployment status read-only; classify deferred command authority; and measure the cost of retaining versus replacing each issuer-dependent integration. Preserve existing accounts, stable mappings, rollout/rollback gates and authorized research access during that assessment.

This inventory is complete for the stated repository scan and representative traces, not exhaustive production assurance. Open gaps are usage confirmation, external repositories/clients, applied overrides, broker policy and end-to-end revocation tests. No runtime remediation, code deletion or activation is included in this documentation PR.

28 September verification follow-up

Three parallel investigations extended the source audit under read-only live-access and isolated synthetic-test constraints. No accounts, grants, credentials, deployed configuration, broker messages or research data were changed. No migration/rehearsal helper, live revocation experiment or remediation was run. Application-owned permissions, R1 (deny subsequent protected operations immediately) and A2 (no implicit site-admin access to protected project content) remain acceptance requirements, not claims that today's implementation meets them.

Applied RabbitMQ permissions and transport

Read-only observation on Juniper, GKE context gke_camarades-net_europe-west2-a_camaradesuk, approximately 2026-09-28 00:59–01:01:44 UTC; final connection snapshot 01:01:44.068 UTC:

Check Observed result Interpretation and limit
Broker user listing Sole returned user rabbit, tagged administrator Listed local users, not an exhaustive inspection of possible authentication backends.
Production vhost syrf-production: rabbit configure/write/read each .* Unrestricted resource access for this principal.
Staging vhost syrf-staging: same user and unrestricted regexes Vhost separation does not isolate environments from this shared principal.
Topic permissions Neither inspected vhost returned rows No additional topic permission rows established; broad resource ACLs are independently observed.
Connections 30 observed, all rabbit, AMQP 0-9-1, ssl=false; 3 production, 4 staging Point-in-time connections, not historical usage or per-workflow attribution.
Listeners AMQP 5672, management HTTP 15672, metrics HTTP 9419, clustering 25672; no AMQPS listener reported Observed AMQP is unencrypted at this layer. Management-ingress TLS does not secure AMQP. Network-layer encryption, exposure and network policy were not assessed.
Applied API/PM/Quartz pods, both environments Allowlisted settings declare rabbit, amqp, port 5672 and the respective vhost Distinct Kubernetes service accounts do not establish distinct broker identities.

Finding: the shared principal has unrestricted publisher/consumer/configuration access across both inspected environments, plus management administrator status. This closes the earlier effective-ACL unknown for this broker and demonstrates missing workload/environment-principal isolation. It is not proof of an exploit or Internet exposure. A private peer-address-to-pod join failed to match connections, including IPv4-mapped normalization; do not attribute the ¾ connections to particular pods or business workflows.

Read-only commands used the existing operator context and the rabbitmq container of rabbitmq/rabbitmq-0:

kubectl config current-context
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmqctl list_users
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmqctl list_vhosts name
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmqctl list_permissions -p syrf-production
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmqctl list_permissions -p syrf-staging
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmqctl list_topic_permissions -p syrf-production
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmqctl list_topic_permissions -p syrf-staging
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmqctl list_connections user vhost ssl protocol --formatter=json
kubectl --request-timeout=15s -n rabbitmq exec rabbitmq-0 -c rabbitmq -- rabbitmq-diagnostics listeners

An initial attempt to pass columns to list_permissions failed with exit 64; the supported default metadata output succeeded. Pod metadata was locally projected to service accounts and allowlisted non-secret broker settings. No password values, Secret resources, raw imported definitions, client properties or message bodies were fetched. Peer addresses used for the unsuccessful private join are not published.

Exposure follow-up (2026-09-30, B0): read-only kubectl get and GitOps reading established that ingress-nginx forwards public rabbitmq.camarades.net:5672 (plaintext AMQP) to the broker, the broker NetworkPolicy admits all sources, and the rabbit-mq ClusterExternalSecret copies the Erlang cookie into every SyRF and preview namespace. The broker security contract holds the evidence, target design and gated rollout.

Separate remediation recommendation: design environment/workload principals, topology-specific ACLs, least-privilege management rights and encrypted AMQP, then validate publisher/consumer topology in synthetic fixtures before an approved rollout. This task did not rotate credentials or change access. Broker hardening does not establish R1/A2, require user JWT forwarding, or justify retaining an Identity-created OAuth boundary.

Actual clients: deployment observed, usage census still unavailable

Read-only Deployment observations at approximately 2026-09-28 00:59–01:02 UTC found:

Environment Ready replicas / safe configuration Reported source metadata
Production API 1, web 3; no Identity Deployment returned by namespace listing API 104eca9b05dceb8a3504eec6ee319f3a6a428283, image tag 9.44.1-restore.104eca9
Staging API 1, Identity 2, web 2, PDF service 1; API explicitly selects Auth0 and enables bulk-PDF upload/notifier authority API 24eae14fa85807b85ed51dfebc39fc724f02e03b; Identity 91617def21fa291a94dcab2bdd2d3d87bbb95c20; web 662b8bdbd51b413729460df2f06fb4b1f281ba7e

These are Deployment-supplied source identifiers and readiness, not independently verified image provenance or successful application calls. They differ from the audit's source baseline. Do not present source-level findings or local test results as proof of deployed behavior. The exact BFF-enabled environment entry checked was absent; absence does not establish its effective value. The ready PDF service does not prove that the separately hosted ARRNC processing worker is active. No pod logs, tokens, account records or Secret values were read.

Deployment discovery used kubectl --request-timeout=15s get deployments -A with custom columns restricted to namespace, name and ready replicas. Scoped JSON was locally projected to SYRF__GitVersion__Sha, the production API image, and staging keys SYRF__IdentityProvider, SYRF__BffAuth__Enabled, SYRF__FeatureFlags__BulkPdfUpload and SYRF__BulkPdfNotifierAuthority__Enabled. A follow-up matching ^SYRF__BffAuth.*Enabled$ also returned no entries. These are allowlisted metadata selections, not a raw environment dump; production authentication configuration was inspected by variable/reference names only.

The available Grafana datasource inventory contained arrnc-prometheus and arrnc-loki. No Loki query was made. Prometheus metric-name discovery for .*(syrf|auth|oauth|openid).* over 2026-09-27T00:00:00Z–2026-09-28T00:00:00Z returned an empty list. This is missing evidence in that one datasource, not zero requests, full Auth0/GCP coverage or evidence that no external clients exist.

Reproducibility: mcp__grafana__list_datasources({}), then mcp__grafana__list_prometheus_metric_names with the following arguments (an initial identical call without the time bounds also returned no matches):

{
  "datasourceUid": "arrnc-prometheus",
  "regex": ".*(syrf|auth|oauth|openid).*",
  "startRfc3339": "2026-09-27T00:00:00Z",
  "endRfc3339": "2026-09-28T00:00:00Z",
  "limit": 100
}

This tool discovers metric names and filters them; it is not a PromQL sample/count query. No label values, request counts or logs were retrieved. An empty name search is especially weak evidence about actual usage.

The inspected telemetry helper's direct-Auth0 path retrieves raw log pages and was not executed under these data-access constraints. BFF telemetry is intentionally aggregate: BffAuthTelemetry.cs:110 tags provider, environment, operation, outcome and status class, not client ID; it cannot alone supply a client census. Seeder source defines the first-party BFF, internal machine and Swagger categories in the inventory; it is neither a live registration census nor proof of actual use. No Identity application collection was queried.

Remaining evidence request: an owner-provided sanitized registry and server-side aggregate export, covering issuer token issuance/refresh and API use, grouped by pseudonymized client/category and grant. Record owner, external repository/integration, audience/scopes and supported lifecycle, without credentials or personal identifiers. Start with a proposed 30-day retrospective, explicitly state retention/collection gaps, and extend it or obtain owner attestation for rare/seasonal clients. That period is not a sufficiency guarantee. Separately confirm living-search consumers, ARRNC processing, Sheets/helpdesk and batch RoB. Approved preaggregated evidence or an operator-generated sanitized report is needed; raw-log access is not an automatic next step. No external delegated/mobile/native population was established or ruled out.

Product scope correction: the 28 September discussion of future machine/delegated external API access was hypothetical and explicitly withdrawn by Chris. It is not an accepted or planned requirement. Restore the first-party browser scope for the ideal recommendation; the census above remains necessary compatibility evidence for real existing integrations, not a reason to invent future clients or preserve Identity circularly. Chris later identified potential eventual external systems authenticating as user accounts; the transition design preserves this as an unresolved future option, not present scope or a required machine/delegated API programme.

Revocation: component evidence, not end-to-end acceptance

222 tests passed, 0 failed, 0 skipped: API 109, Identity 113. Four TRX counter summaries were independently checked during integration. These use xUnit/Moq with synthetic identifiers, mocked Identity managers/HTTP/ repositories/SignalR and in-memory distributed cache. They did not contact a real issuer, Redis, Mongo or broker. The two-pod publication theory was explicitly excluded (including both rows), because one row starts RabbitMQ. Whole-host BFF integration tests were not selected. No application files changed; git diff 0026b56a80fe85f1aea0f690663b02663de6899e -- src was empty. Build warnings were not test failures.

Surface Evidence / result Remaining R1/A2 gap
Role removal Effective-group tests pass; EffectiveApplicationGroups.cs:23 unions claims and PM roles Removing a PM role does not withdraw a retained claim grant. Passing tests preserve current behavior; they do not prove the target.
Existing native BFF session Session-handler tests verify per-request introspection and denial on inactive/unavailable validation; SessionAuthenticationHandler.cs:106 PM role/suspension edits do not automatically invoke token withdrawal. Auth0 sessions do not use native introspection.
BFF refresh Controller/session tests pass; BffAuthController.cs:620 replaces tokens while retaining session groups Token renewal is not group refresh.
Native refresh/direct bearer Issuance tests verify generation, evidence, mapping and admission gates; UserClaimsService.cs:45 emits stored Identity groups. API Program.cs:480 disables native introspection caching Native token validity is distinct from fresh PM authorization; no real direct-token protocol experiment was run.
Ordinary HTTP/SignalR checks AuthorizationHandler.cs:68 and SignalRAuthorizationHandler.cs:49 use cached Investigator/project reads; no generic suspension guard in those handlers Uniform application-suspension denial is not established.
Replicas/cache RepositoryCache.cs:38 uses a two-second process-local positive cache; save eviction is local No synchronous cross-replica R1 guarantee. Two seconds is not an accepted grace period; no multi-replica experiment ran.
Existing SignalR streams Disabled-membership tests stop presence/full-statistics delivery, prevent resubscription and prevent late-query/stale-context restoration after observed revocation Component disablement tests, not every physical membership-removal path or immediate cross-replica propagation.
Statistics/claim-event delivery Consumer tests deny disabled/former members or deactivated Investigators. Statistics delivery rereads uncached state, then unions saved subscription claims Helpful current-recipient checks; not universal removal of stale role grants or proof of activated runtime paths.
Queued work Screening consumer checks pending workflow state; UpdatedBy is logging data in the traced path No execution-time actor recheck proven. Classify admitted completion versus a new delegated action before asserting a bypass.

A2 qualification: ProjectPermission.cs:58 permits a configured application-level grant before active membership. This is not a universal hard-coded administrator bypass. In ResourceSecurity.json:107, project View allows all application users, whereas ViewStudies does not. Classify public project summary versus protected content and test administrator nonmembers against content/read/write/export operations; do not infer universal access from the shortcut or universal safety from one configuration entry.

Exact commands from the audit worktree:

dotnet test src/services/api/SyRF.API.Endpoint.Tests/SyRF.API.Endpoint.Tests.csproj --filter '(FullyQualifiedName~BffSessionTokenValidatorTests|FullyQualifiedName~BffSessionIsolationTests|FullyQualifiedName~EffectiveApplicationGroupsTests|FullyQualifiedName~NotificationHubDisabledMembershipRevocationTests|FullyQualifiedName~ActivityClaimRevokedConsumerTests|FullyQualifiedName~ProjectStatisticsNotificationTests|FullyQualifiedName~SignalRAuthorizationHandlerTests)&FullyQualifiedName!~PublishedInvalidationReachesTwoIndependentPodQueues' --nologo --logger 'trx;LogFileName=revocation-api.trx'
dotnet test src/services/identity/SyRF.Identity.Endpoint.Tests/SyRF.Identity.Endpoint.Tests.csproj --filter 'FullyQualifiedName~IdentitySessionRevocationServiceTests|FullyQualifiedName~AuthorizationControllerTests|FullyQualifiedName~AuthorizationControllerMappingGateTests' --nologo --logger 'trx;LogFileName=revocation-identity.trx'
dotnet test src/services/identity/SyRF.Identity.Endpoint.Tests/SyRF.Identity.Endpoint.Tests.csproj --no-build --no-restore --filter 'FullyQualifiedName~AuthorizationControllerFailClosedIssuanceTests' --nologo --logger 'trx;LogFileName=revocation-issuance.trx'
dotnet test src/services/api/SyRF.API.Endpoint.Tests/SyRF.API.Endpoint.Tests.csproj --no-build --no-restore --filter 'FullyQualifiedName~SessionAuthenticationHandlerTests|FullyQualifiedName~BffAuthControllerTests' --nologo --logger 'trx;LogFileName=revocation-session.trx'

Counts in order: 63, 43, 70, 46. The first Identity filter included a nonexistent AuthorizationControllerMappingGateTests name; no coverage is claimed for it. The subsequent explicit AuthorizationControllerFailClosedIssuanceTests run supplied issuance-gate coverage. Ignored local artifacts are each project's TestResults/revocation-*.trx; they contain synthetic test results and are not committed.

Concrete remaining end-to-end test contract

The missing environment is a purpose-built disposable native-authentication stack, not permission to experiment on production or rehearsal accounts: two API replicas, native Identity, shared isolated Redis, Mongo replica set, RabbitMQ and PM worker, all with synthetic data. Generic e2e login bypasses OIDC, so those tests cannot alone establish real issuance/session-revocation behavior. No such stack was started here. That stack now exists as the opt-in M0a lane e2e/authority/ (how-to); items 1–3 are exercised there for role removal and deactivation as expected-failing R1 characterizations. M1a adds an enforced run mode (--application-roles-mode enforced) in which PM authority decides ListUsers and the two ListUsers characterizations pass. M1b makes enforced the lane default and proves grant/revoke through the PM role API on both replicas with old sessions, direct tokens and refreshed sessions, concurrent last-administrator removal and the administrator-nonmember (A2) project case. M1c adds account deletion, Identity's destructive calls and their races with role removal to the same last-administrator guard, with the real Identity. M2a runs every other application capability (email templates, batch administration, runtime flag management) through the same mode contract on both replicas with sessions and a direct token, including a revoke. Items 4–7 remain for later milestones.

  1. Establish native BFF sessions, refresh/direct tokens and sockets on both replicas for synthetic role holders, project members, administrator nonmembers and suspension candidates. Warm positive caches.
  2. Separately commit PM role removal, membership disablement, physical membership removal and application suspension through an authoritative test operation. Record the acknowledged commit boundary without recording credentials. Define the supported mutation contract where no production operation exists.
  3. Start protected requests after commit, using retained credentials on both replicas: immediate denial, no cache/refresh grace. Test protected content/read/write/export, not only /me. Require explicit audited support authority for any administrator nonmember exception; distinguish public metadata.
  4. Try refresh and fresh login, distinguishing valid authentication from denied application permissions. Repeat with old application claims and a role-claim-free principal; neither transport earns authority.
  5. Hold socket queries at controlled barriers, commit revocation, then release them: no protected delivery or resubscription on either replica. Cover already-connected principals and each delivery path.
  6. Queue three categories before the mutation, then release them locally: new deferred user action (current execution authority), admitted durable obligation (explicit completion/cancellation contract), committed fact (process idempotently, independently authorize current recipients). Agree these contracts before treating every continued job as a revocation defect.
  7. Exercise issuer/store unavailability and stale snapshots fail-closed. Define in-flight boundaries; revocation cannot retract bytes already delivered or justify replaying irreversible actions.

Integrated conclusion and next approval boundary

  • Verified runtime security finding: broad shared broker authority and non-TLS AMQP in the observed broker. Plan separately approved, staged remediation; do not alter access from this audit.
  • Client usage remains blocked on evidence: deployed readiness and a narrow empty metrics search do not authorize issuer removal. Obtain the sanitized owner/usage census, preserving separate batch RoB, and keep the withdrawn hypothetical external-client discussion out of the product requirements.
  • R1 is not certified: passing component tests coexist with source-level additive claims, local caching and inconsistent suspension checks. Build the isolated native multi-replica acceptance fixture before promising immediate global denial. A2 needs endpoint-specific protected-content assertions.
  • Architecture decisions remain separate: application-owned permissions/no application-role claims, browser cookie/BFF transport and machine authentication. Neither broker hardening nor Identity's own calls establish that the current OAuth issuer must remain; neither permits removing it without compatibility evidence.

No remediation, authentication redesign, rollout or merge is approved by these findings. The documentation records bounded verification and concrete residual work; it does not convert unknowns into acceptance.