Fair Supply LogoFair Supply - Docs

Internal FSAdmin Activity Visibility

This rule governs which reports and engagements a non-admin viewer sees when they belong to a client account where FSAdmin users have authored work (typically via impersonation). It ensures analytical surfaces (dashboard, assessments) present a client-faithful view of activity rather than a mix of client work and Fair Supply staff work.

The Rule

If the viewer does not have the FSAdmin Auth0 role, hide all activity authored by internal (Fair Supply) users — including its contribution to derived metrics.

Symmetric across reports and engagements. Applied at the application (use case) layer, post-fetch, before any aggregation.

Why this exists

FSAdmin users can impersonate a client account (Redis-backed session selection, no BELONGS_TO relationship written). When impersonating, they can author Spotlight/Analyst reports, supply-chain assessments, and engagements that get persisted as Neo4j nodes attributed to the FSAdmin's User record.

Without this rule, the real client (a member with BELONGS_TO to the account) would see:

  • Rows in the activity table they did not create and cannot trace back to anyone in their organisation.
  • Metrics (Risk Assessments, Supplier Engagements, Mitigation Actions, Supply Chain Risk) inflated by Fair Supply's internal work.
  • A discrepancy between counts and what is visible in the table.

Where the rule applies

SurfaceUse caseFilter applied to
Dashboard (Activity tab)GetAccountActivityreports, engagements, orgRiskMap, all activity metrics, all overview metrics
Assessments pageGetAssessmentsreports only (assessments domain has no engagements)

Where the rule does not apply

  • Active Suppliers (overviewMetrics.activeOrganisations) — derived from OrganisationRepository.findActiveOrganisationCount, transaction-based, not author-attributable.
  • Active Spend (overviewMetrics.activeSpend) — pure transaction aggregate.
  • FSAdmin viewers — pass-through; admins see all activity in any account they have selected (including impersonated ones), so impersonation work remains visible to the impersonating admin.

How "internal user" is identified

Phase 1 (current) — email-domain heuristic

isInternalUserEmail(email) in packages/core/src/domain/utils/internal-user.ts returns true for any address ending in @fairsupply.com.au or @fairsupply.com.

This is a pragmatic stop-gap that ships immediately and requires no schema changes. Trade-offs:

  • False positives: a non-staff user with a Fair Supply email (rare, edge cases like partner accounts) would have their work hidden from non-admin viewers.
  • False negatives: an FSAdmin with a non-FS email (e.g., a contractor with role granted but external mailbox) would not be filtered.

Phase 2 (planned) — persisted createdViaImpersonation flag

Tracked in FairSupply/platform#1122.

Replace the heuristic with a boolean property on Report and Engagement nodes, set at write time when the author's Auth0 user is operating in an account they do not have BELONGS_TO to (i.e., impersonation context). Push the filter down to the GraphQL where clause so the database does the work.

Why this is better:

  • Semantically correct — captures the actual concern ("this came from impersonation") rather than a proxy (email domain).
  • Stable — role changes and email changes don't drift the signal.
  • Auditable — gives a direct query for "all impersonated work in this account".
  • Performant — DB-side filter avoids over-fetch.

Backfill plan: use Phase 1 heuristic to backfill the flag on existing records during the migration.

The trust boundary

Use cases that apply this rule accept isAdmin: boolean as a required input. The caller (presentation layer) is responsible for deriving this from hasAdminRole() in apps/web/src/lib/auth.ts.

Passing isAdmin: true without verifying via hasAdminRole() is a security bug — equivalent to bypassing the filter. Use cases trust this input; they do not re-verify against Auth0 (that would require a domain-layer dependency on the Auth0 SDK, which is a Clean Architecture violation).

This mirrors the contract documented on ImpersonateAccountAsAdmin in packages/core/src/application/use-cases/account/impersonate-account-as-admin.ts.

Mixed-source orgs

An organisation that appears in both a client-authored engagement and an FSAdmin-authored report retains its risk contribution via the client engagement. orgRiskMap uses a "first writer wins" pattern (if (map.has(id)) continue) while iterating these sources:

  • orgRiskMap: engagement orgs → report orgs → account-scoped organisations

The client engagement seeds the org before the FSAdmin report is processed, so the report's risk data does not leak through to the non-admin viewer's metrics.

If an org appears only in FSAdmin-authored activity for a non-admin viewer, the org contributes nothing to supply-chain risk. The viewer sees a smaller, client-faithful supply-chain picture.

Adding new surfaces

When introducing a new use case that aggregates reports or engagements for analytical display:

  1. Add isAdmin: boolean as a required input field.
  2. Document the security contract on the field (see GetAssessmentsInput.isAdmin for the canonical pattern).
  3. Immediately after fetching reports/engagements, create visibleReports / visibleEngagements using the heuristic:
    const visibleReports = input.isAdmin
      ? reports
      : reports.filter((r) => !isInternalUserEmail(r.createdBy[0]?.email));
  4. Use the filtered variants in every downstream consumer — rows, metrics, derived maps.
  5. Wire isAdmin through from the presentation layer using hasAdminRole().
  6. Add a test covering the non-admin and admin paths.
  • Accounts Domain — covers Auth0 roles, sessions, and the impersonation mechanism.
  • GetAccountActivity in packages/core/src/application/use-cases/account/get-account-activity.ts — reference implementation for the symmetric report + engagement filter.
  • GetAssessments in packages/core/src/application/use-cases/assessment/get-assessments.ts — reports-only mirror.

On this page