You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Port the existing .NET (C#) identity-resolution service to Rust at behavioural
parity, cut all traffic over to it via api-gateway, and decommission the .NET
service. The matching/mapping logic itself (how accounts collapse into a
canonical person) is specified in its own epic — see Relations; this epic is the
service reimplementation, not new resolution behaviour.
Context
The identity store is already live (BambooHR + Entra with a reconcile loop). What
remains is moving the running implementation off .NET onto Rust without changing
observable behaviour, then retiring the old service.
Hard constraints — no administration surface, and no administrator exists
Two separate facts, both verified against main (2026-07-27). Together they
determine what can actually be tested today.
1. There is no administrative user interface. Everything below is a service
capability, not a screen. The product has no administration area: no screen for
access grants, roles, people/linked-account management, or triggering an identity
refresh. What the UI does show: a person + team view (/ic/<person>), a metrics
view, a what's-new page — all read-only consumers of identity data.
2. There is no administrator in any environment. The mechanism is fully built
(admin-gated operations reject callers without an active admin role), but no role
holder is ever created: seeding the first admin is optional config, is not set
anywhere in the deployment configuration, and when unset the seeding step is a silent no-op (it also requires a default company id, itself a placeholder). Net effect: nobody is an administrator, so every administrative operation is
refused for everyone.
What this means for testing
"Administrator" is a role attached to a request, not a person with a console.
Do not write criteria as "the administrator opens X and clicks Y".
Every requirement below is testable only at service level.
Testable with no setup: BR-7 (see yourself), BR-8 (own reporting chain, any
depth), BR-9/BR-15 (refused for anyone outside it). Prioritise these — they are
what actually governs access today.
Blocked without seeding an admin first: acceptance criteria 4, 5, 6, 11.
Without that step they cannot pass, and that is current expected behaviour,
not a defect.
The grant-based half of the access model (BR-10, BR-11) is not exercisable in
a default install.
Stated in business terms so acceptance criteria can be written and tested without
reading code. This is current behaviour — the port must preserve all of it. BR-x ids are usable as test-case ids.
Actors
Actor
Definition
Employee
Any signed-in person in the company
Manager
An employee with people reporting to them, directly or further down
Grantee
An employee explicitly given access to see another person
Administrator
Holder of the admin role — permits changing grants and roles. Service-level only; see Hard constraints
System
Insight's own internal sign-in process
What the product knows about a person
BR-1 For each employee Insight maintains one canonical person record,
regardless of how many separate accounts they hold in connected tools.
BR-2 A person record carries: name (full, first, last), work email, job
title, department, division, employment status, employee id, username, manager.
BR-3 A person record lists all of that person's known accounts across
connected systems — one entry per system and account. This is the mapping that
makes every metric attributable to a real human.
BR-4 A person record lists that person's direct reports.
BR-5 One human must resolve to exactly one person record. If a lookup
could mean two different people, Insight refuses to answer and reports the
conflict, naming the conflicting records. It must never quietly pick one — a
wrong pick attributes someone's work to another person.
BR-6 A person can be looked up either by work email or by an account
identifier from a connected system; both routes return the same record.
Who is allowed to see whom
BR-7 (see yourself) Every employee can always see their own record.
BR-8 (managers see their org) A manager can see everyone below them in the
reporting chain, at any depth — not only direct reports.
BR-9 (nothing else by default) By default an employee cannot see their
manager, their peers, or anyone outside their own reporting chain.
BR-10 (explicit grant) A grant can be created giving an employee access to
another person. It carries the whole reporting chain below that person, not
just the one person — granting on a department head grants the department.
BR-11 (company-wide grant) A grant can be created giving access to everyone in the company.
BR-12 (administrator is not a viewer) Holding admin permits changing
grants and roles. It does not grant access to see people's data. An
administrator with no grant sees only themself and their own chain. Deliberate —
confirm it, don't file it as a bug.
BR-12a Because no administrator exists (see Hard constraints), all
administrative operations are currently refused for every caller until one is
seeded. This is current expected behaviour.
BR-13 (time-bounded) Every grant has a start, can be revoked, and grant
history is retained.
BR-13a (as-of a past date — org chart only) "How did the organisation and
access look on date D" can be asked only of the organisation-chart views.
Person/profile lookups always evaluate access as of now and accept no past
date; a future date is rejected as invalid.
BR-14 (auditability) Every grant records who created it and optionally why.
Grants are never edited or erased — a revoke is recorded as an end date.
BR-15 (no existence leak) When an employee asks about a person they may not
see, Insight answers exactly as if that person did not exist. "No such
person" and "not allowed" must be indistinguishable — the difference alone would
leak the org structure.
Viewing the organisation chart
BR-16 An employee can view the reporting tree from any person they may see,
limiting how many levels deep.
BR-17 With no depth limit, the whole chain below that person is returned.
BR-18 Asking for "my organisation" with no named root returns exactly:
The employee's access
What they see
No grants
One tree: themself + their own chain
Grant on a peer
Two trees: their own, plus the peer's
Grant on their manager
One tree rooted at that manager (they appear inside it)
Company-wide grant
Every top-level tree in the company
BR-19 An employee with no place in the organisation data gets an empty
result, not an error.
BR-20 People with no manager and no reports are not shown as one-person
"trees". The record is still kept — "employee in HR with no manager assigned" is
a data-quality signal — but it is not presented as an organisation.
BR-21 Each node shows name, work email, job title, employment status.
Keeping identity data current
BR-22 Identity data is populated from connected HR/directory systems. A
refresh can be triggered, runs in the background, and its outcome can be checked
— the requester is not blocked.
BR-23 Reporting structure is stored with history, so past states remain
answerable (BR-13a).
The migration itself
BR-24 From a business standpoint the requirement is no observable
change: every rule BR-1…BR-23 must behave identically before and after, for the
same inputs. Any behavioural difference is a defect, not an improvement.
BR-25 (one deliberate exception) The old email-in-the-URL lookup is intentionally not carried over — superseded by a lookup that does not put an
email address into a web address. This is the single accepted difference.
Current state of this migration — verify before testing
Verified against main, 2026-07-27. Both services are deployed side by side.
The new service is enabled by default, runs against the same identity database,
and its migration step now owns that schema.
Question
Answer today
New service built and deployed?
Yes — enabled by default, side by side
Does it receive user traffic?
No, not by default
Who serves users?
The old service, unless an environment is explicitly flipped
The cutover is a switch: one configuration value points the consumers
(analytics, sign-in) at either implementation. Empty = the old service (historical
default), so existing environments are untouched. Flipping it plus the gateway
route moves traffic; clearing it rolls back (consumers read the value at start-up
and must be restarted).
Consequences for QA:
By default, tests exercise the old service. Confirm which implementation the
target environment points at — otherwise a green run says nothing about the port.
Test both sides of the switch, including rollback.
Because both services share one database and the new one owns the schema,
include a check that the old service still behaves correctly after the new one
has migrated it — a one-sided parity test would miss this.
"Old service retired" is not done. Side-by-side deploy and a working switch
are done.
Not true today — do not claim in acceptance criteria
Metric data is not access-controlled. The access rules above govern identity and org-chart data only. Any signed-in member can currently request
metrics for any person in the company. So "a manager sees only their
subordinates' numbers" is true for people data and false for metric data. See INGEST 1: Unified metric system (Metrics catalog) #1561.
No administration surface and no administrator — see Hard constraints.
Acceptance Criteria
All service-level tests — there is no administrative screen, so setup and
verification are done by calling the service. Criteria 4, 5, 6 and 11 require
seeding a first administrator; without it they cannot pass and must not be
reported as failures. Criteria 1, 2, 3 and 10 need no setup — run those first.
An employee can retrieve their own record, with accounts and direct reports
listed. (BR-1…BR-4, BR-7)
A manager can retrieve records for people two or more levels below them. (BR-8)
An employee cannot retrieve records for their manager, a peer, or an unrelated
person, and the response is indistinguishable from "no such person". (BR-9, BR-15)
After a grant on a department head is created, the grantee can retrieve every
person in that department; after revoke, they cannot. (BR-10, BR-13)
A company-wide grant lets the grantee retrieve any person. (BR-11)
A newly created administrator with no grant can still only see themself and
their own chain. (BR-12)
Asking for the organisation chart as of a past date reflects the grants and
reporting lines in force then, not now; a future date is rejected. Person and
profile lookups offer no past-date option. (BR-13, BR-13a)
A lookup matching two people returns a conflict naming both records, and no
profile is returned for either. (BR-5)
A person is found both by email and by a connected-system account id, and both
return the same record. (BR-6)
"My organisation" returns exactly the tree set in the BR-18 table for each of
the four access states, an empty result for an employee outside the org data,
and no one-person trees. (BR-18…BR-20)
Identity refresh can be triggered and its outcome inspected without blocking
the caller. (BR-22)
Every rule above yields the same result on the new implementation as the old,
for the same fixture data — with the email-in-the-URL lookup being the one
accepted, documented removal. Precondition: confirm which implementation the
environment points at. (BR-24, BR-25)
Switching the environment to the new implementation, and switching it back,
both leave the product working — no user-visible change either way. (BR-24)
The old implementation still behaves correctly after the new one has taken
ownership of the shared database schema. (BR-24)
Vectors define the metrics quality must reach; the tests below are the automation that fulfills them — one test may cover several vectors. The Rust port must resolve accounts to one human at behavioural parity with the C# service — merging two people corrupts every metric downstream, so parity leads the risk. Speed and memory run on the 3,000-person demo org.
Quality vectors — at a glance
Vector
Metric
Target
Collected today
Reliability
API coverage, both implementations
all documented operations · all answerable codes
C# 18/18 ops, 100% codes · Rust 17/18, 94.4% — the single gap is GET /v1/persons/{email}, the endpoint the port deliberately dropped (BR-25, accepted exemption)
Versatility
—
Not applicable
Performance
Person-lookup latency
P95 < 100 ms at every concurrency level
not collected — no load harness in the repo
Efficiency
Rust service CPU/RAM vs the C# service
≤ 100% of C# for both · 0 MB/h growth
not collected — same missing harness; must run while both services are still deployed
Security
Findings in the service's own files
0 critical
collected — 0 (one low: missing HEALTHCHECK in services/identity-resolution/Dockerfile)
Tests
Identity contract suite, both sides of the switch — run the implementation-agnostic e2e suite (test(e2e): identity API contract suite — implementation-agnostic (#1753) #1897) against the C# and the Rust service, asserting every documented operation and response code via the two blocking endpoint-coverage gates.
Covers: Reliability · Status:done — both gates green on main.
Dual-service soak — drive ≥ 1 h of continuous lookups on the same org against the Rust and the C# service in turn, recording CPU/RSS (Rust ≤ 100% of C#) and memory growth (0 MB/h) in the same run.
Covers: Efficiency · Status:to write — run it before the .NET service is decommissioned, or the baseline is gone.
Security needs no separate test: Semgrep (main, report-only #1797) and Trivy (nightly) already scan the port's files — 0 critical; insight-identity and insight-identity-resolution images are scanned nightly and clean.
Canonical technical spec: docs/domain/identity-resolution/specs/PRD.md
(⚠️ dated; verify mechanics against current design before implementing).
Planning:
Assignee: Sergey Mozhaev
Estimation (days): TBD — re-split from the original 25 across #1602 + GRAPH 1b
Estimation for QA (days): TBD — re-split from the original 6 across #1602 + GRAPH 1b
Goal
Port the existing .NET (C#) identity-resolution service to Rust at behavioural
parity, cut all traffic over to it via api-gateway, and decommission the .NET
service. The matching/mapping logic itself (how accounts collapse into a
canonical person) is specified in its own epic — see Relations; this epic is the
service reimplementation, not new resolution behaviour.
Context
The identity store is already live (BambooHR + Entra with a reconcile loop). What
remains is moving the running implementation off .NET onto Rust without changing
observable behaviour, then retiring the old service.
Hard constraints — no administration surface, and no administrator exists
Two separate facts, both verified against
main(2026-07-27). Together theydetermine what can actually be tested today.
1. There is no administrative user interface. Everything below is a service
capability, not a screen. The product has no administration area: no screen for
access grants, roles, people/linked-account management, or triggering an identity
refresh. What the UI does show: a person + team view (
/ic/<person>), a metricsview, a what's-new page — all read-only consumers of identity data.
2. There is no administrator in any environment. The mechanism is fully built
(admin-gated operations reject callers without an active
adminrole), but no roleholder is ever created: seeding the first admin is optional config, is not set
anywhere in the deployment configuration, and when unset the seeding step is a
silent no-op (it also requires a default company id, itself a placeholder).
Net effect: nobody is an administrator, so every administrative operation is
refused for everyone.
What this means for testing
Do not write criteria as "the administrator opens X and clicks Y".
depth), BR-9/BR-15 (refused for anyone outside it). Prioritise these — they are
what actually governs access today.
Without that step they cannot pass, and that is current expected behaviour,
not a defect.
a default install.
surface is separate work, called out as required in GRAPH 1b: Identity mapping engine — resolve accounts to a canonical person #1873.
Scope
person_id) endpoints./v1/subchart*), roles + person-roles(
/v1/roles*,/v1/person-roles*), visibility (/v1/visibility*). (identity-resolution: port remaining domains + cutover #1755)decommission the .NET identity service.
Out of scope
Business requirements (as-is)
Stated in business terms so acceptance criteria can be written and tested without
reading code. This is current behaviour — the port must preserve all of it.
BR-x ids are usable as test-case ids.
Actors
adminrole — permits changing grants and roles. Service-level only; see Hard constraintsWhat the product knows about a person
regardless of how many separate accounts they hold in connected tools.
title, department, division, employment status, employee id, username, manager.
connected systems — one entry per system and account. This is the mapping that
makes every metric attributable to a real human.
could mean two different people, Insight refuses to answer and reports the
conflict, naming the conflicting records. It must never quietly pick one — a
wrong pick attributes someone's work to another person.
identifier from a connected system; both routes return the same record.
Who is allowed to see whom
reporting chain, at any depth — not only direct reports.
manager, their peers, or anyone outside their own reporting chain.
another person. It carries the whole reporting chain below that person, not
just the one person — granting on a department head grants the department.
everyone in the company.
adminpermits changinggrants and roles. It does not grant access to see people's data. An
administrator with no grant sees only themself and their own chain. Deliberate —
confirm it, don't file it as a bug.
administrative operations are currently refused for every caller until one is
seeded. This is current expected behaviour.
history is retained.
access look on date D" can be asked only of the organisation-chart views.
Person/profile lookups always evaluate access as of now and accept no past
date; a future date is rejected as invalid.
Grants are never edited or erased — a revoke is recorded as an end date.
see, Insight answers exactly as if that person did not exist. "No such
person" and "not allowed" must be indistinguishable — the difference alone would
leak the org structure.
Viewing the organisation chart
BR-16 An employee can view the reporting tree from any person they may see,
limiting how many levels deep.
BR-17 With no depth limit, the whole chain below that person is returned.
BR-18 Asking for "my organisation" with no named root returns exactly:
BR-19 An employee with no place in the organisation data gets an empty
result, not an error.
BR-20 People with no manager and no reports are not shown as one-person
"trees". The record is still kept — "employee in HR with no manager assigned" is
a data-quality signal — but it is not presented as an organisation.
BR-21 Each node shows name, work email, job title, employment status.
Keeping identity data current
refresh can be triggered, runs in the background, and its outcome can be checked
— the requester is not blocked.
answerable (BR-13a).
The migration itself
change: every rule BR-1…BR-23 must behave identically before and after, for the
same inputs. Any behavioural difference is a defect, not an improvement.
intentionally not carried over — superseded by a lookup that does not put an
email address into a web address. This is the single accepted difference.
Current state of this migration — verify before testing
Verified against
main, 2026-07-27. Both services are deployed side by side.The new service is enabled by default, runs against the same identity database,
and its migration step now owns that schema.
The cutover is a switch: one configuration value points the consumers
(analytics, sign-in) at either implementation. Empty = the old service (historical
default), so existing environments are untouched. Flipping it plus the gateway
route moves traffic; clearing it rolls back (consumers read the value at start-up
and must be restarted).
Consequences for QA:
target environment points at — otherwise a green run says nothing about the port.
include a check that the old service still behaves correctly after the new one
has migrated it — a one-sided parity test would miss this.
against either service. Reuse it rather than writing parity tests from scratch.
are done.
Not true today — do not claim in acceptance criteria
identity and org-chart data only. Any signed-in member can currently request
metrics for any person in the company. So "a manager sees only their
subordinates' numbers" is true for people data and false for metric data. See
INGEST 1: Unified metric system (Metrics catalog) #1561.
scheduled re-sync, so reorganisations don't reach the Team view on their own
(Team view shows a stale roster — manager and org changes never appear #1690).
analytics layer does not filter by company.
?__override=<email>) no longer works #1941); the underlying trust model is beinghardened (Enforce real permissions: derive admin/access rights from verified identity, not client-supplied X-Insight-* headers #1769).
Acceptance Criteria
All service-level tests — there is no administrative screen, so setup and
verification are done by calling the service. Criteria 4, 5, 6 and 11 require
seeding a first administrator; without it they cannot pass and must not be
reported as failures. Criteria 1, 2, 3 and 10 need no setup — run those first.
listed. (BR-1…BR-4, BR-7)
(BR-8)
person, and the response is indistinguishable from "no such person".
(BR-9, BR-15)
person in that department; after revoke, they cannot. (BR-10, BR-13)
their own chain. (BR-12)
reporting lines in force then, not now; a future date is rejected. Person and
profile lookups offer no past-date option. (BR-13, BR-13a)
profile is returned for either. (BR-5)
return the same record. (BR-6)
the four access states, an empty result for an employee outside the org data,
and no one-person trees. (BR-18…BR-20)
the caller. (BR-22)
for the same fixture data — with the email-in-the-URL lookup being the one
accepted, documented removal. Precondition: confirm which implementation the
environment points at. (BR-24, BR-25)
both leave the product working — no user-visible change either way. (BR-24)
ownership of the shared database schema. (BR-24)
Sub-tasks
Testing
Vectors define the metrics quality must reach; the tests below are the automation that fulfills them — one test may cover several vectors. The Rust port must resolve accounts to one human at behavioural parity with the C# service — merging two people corrupts every metric downstream, so parity leads the risk. Speed and memory run on the 3,000-person demo org.
Quality vectors — at a glance
GET /v1/persons/{email}, the endpoint the port deliberately dropped (BR-25, accepted exemption)HEALTHCHECKinservices/identity-resolution/Dockerfile)Tests
Identity contract suite, both sides of the switch — run the implementation-agnostic e2e suite (test(e2e): identity API contract suite — implementation-agnostic (#1753) #1897) against the C# and the Rust service, asserting every documented operation and response code via the two blocking endpoint-coverage gates.
Covers: Reliability · Status: done — both gates green on main.
Lookup latency ramp — k6 email/login → person lookups against the 3,000-user org, single-user baseline first then ramped concurrency, asserting P95 < 100 ms at every level.
Covers: Performance · Status: to write — shared harness with INGEST 1: Unified metric system (Metrics catalog) #1561/Split Insight into an engineering (data contract) layer and a presentation layer #1803.
Dual-service soak — drive ≥ 1 h of continuous lookups on the same org against the Rust and the C# service in turn, recording CPU/RSS (Rust ≤ 100% of C#) and memory growth (0 MB/h) in the same run.
Covers: Efficiency · Status: to write — run it before the .NET service is decommissioned, or the baseline is gone.
Security needs no separate test: Semgrep (main, report-only #1797) and Trivy (nightly) already scan the port's files — 0 critical;
insight-identityandinsight-identity-resolutionimages are scanned nightly and clean.Relations
engine epic (GRAPH 1b: Identity mapping engine — resolve accounts to a canonical person #1873, companion to this one).
docs/domain/identity-resolution/specs/PRD.md(
Planning:
Assignee: Sergey Mozhaev
Estimation (days): TBD — re-split from the original 25 across #1602 + GRAPH 1b
Estimation for QA (days): TBD — re-split from the original 6 across #1602 + GRAPH 1b