feat(identity-resolution): remaining domains — roles, person-roles, visibility, subchart, internal lookup (#1755) - #1862
Conversation
First domain of the remaining .NET port: admin-gated CRUD over the global
`roles` table, 1:1 with the .NET RolesEndpoints (ADR-0013).
- GET /v1/roles — list all roles (ListResponse shape, next_cursor null).
- POST /v1/roles — create; validates name (non-empty, <=64); pre-checks the
unique name for a friendly 409 (already_exists) instead of an opaque 500;
201 + Location /v1/roles/{id}.
- DELETE /v1/roles/{id} — hard delete with an atomic in-use guard
(TryDeleteRoleIfUnused); 204 on success, 404 if missing, and a precondition
error when the role still has active person_roles.
Infra: roles_repo gains Role + get_by_name/get_by_id/list_all/insert_role/
try_delete_if_unused/count_active_assignments_any_tenant (SQL verbatim from
Sql.Roles.cs). Extract the shared admin gate (require_admin/resolve_caller) out
of seed.rs into api/gate.rs so roles/person-roles/visibility reuse it; add
AccessError (gate 403) + RoleError.
Divergence (documented): the .NET "role in use" 422 has no gears canonical
equivalent, so it is surfaced as failed_precondition (400) — same spirit as the
gts:// vs urn: error-type divergence.
Tests: role-name validation + resolve_caller (moved with the gate). clippy
clean. Live business-logic e2e is deferred to the gateway-JWT harness (#1753):
post-#1777 the host enforces gateway-JWT auth, so endpoints now require a signed
JWT — verified here that routes register and the authn gate returns canonical 401.
Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
Second domain: admin-gated grant / list / revoke over the `person_roles`
junction, 1:1 with the .NET PersonRolesEndpoints (ADR-0014).
- POST /v1/person-roles — grant; validates person_id/role_id present;
valid_from defaults to now; 201 + Location + assignment (read back).
- GET /v1/person-roles — list with ?person= / ?role= / ?active= (default all) /
?limit= (default 50, cap 500), newest first.
- DELETE /v1/person-roles/{id} — revoke (soft-delete via valid_to); optional
{reason} body; refuses to remove the tenant's LAST active admin (lockout
guard) via the atomic TrySoftDeletePersonRoleProtectingLastAdmin UPDATE.
Infra: new person_roles_repo (PersonRole + get_by_id/list/insert/
try_soft_delete_protecting_last_admin) — SQL verbatim from Sql.Roles.cs, incl.
the last-admin subquery + positional bind order. New PersonRoleError.
Divergence (documented): the .NET last-admin 422 has no gears equivalent →
failed_precondition (400), consistent with the roles domain.
Tests: id-presence validation unit test (the DB-level guard/filters are covered
by the deferred #1753 integration tests). clippy clean, 36 tests.
Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
Third domain: admin-gated create / list / revoke over the `visibility` table,
1:1 with the .NET VisibilityEndpoints (ADR-0012).
- POST /v1/visibility — grant; validates viewer_person_id present + reason
<=500; viewed_person_id optional (null = whole-tree visibility); valid_from
defaults to now; 201 + Location + grant (read back).
- GET /v1/visibility — list with ?viewer= / ?viewed= / ?active= (default all) /
?limit= (default 50, cap 500), newest first.
- DELETE /v1/visibility/{id} — soft-delete (set valid_to); optional {reason}
body; 404 only if the grant never existed, else 204 (idempotent re-revoke).
Infra: new visibility_repo (Visibility + get_by_id/list/insert/soft_delete),
SQL verbatim from Sql.Visibility.cs; new VisibilityError. No lockout guard here
(unlike person-roles) — revoke is a plain soft-delete.
Tests: reason-length validation. clippy clean, 37 tests.
Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
…-resolution-domains-cutover # Conflicts: # src/backend/services/identity-resolution/src/api/seed.rs
…-resolution-domains-cutover
…sibility lists (#1755) Consistency with the persons-seed list (@cyberantonz review): leaner {items} shape, no always-null cursor field. Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
…-resolution-domains-cutover
… blockers (#1755) The role in-use (correlated NOT EXISTS) and last-admin (UPDATE...JOIN count) guards have no toolkit-db builder form — annotate them alongside the central infra::db rationale. See constructorfabric/gears-rust#4239. Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
…-resolution-domains-cutover
…-resolution-domains-cutover
…-resolution-domains-cutover
…#1755) Port GET /internal/persons/by-email/{email} from the .NET PersonsEndpoints — the login-bootstrap resolver the authenticator calls before a tenant or caller identity exists. It deliberately bypasses the tenant + visibility gates the public /v1/profiles enforces, but stays fail-closed: a valid gateway JWT is required (host authn) and a non-service principal (subject_type != "service", the gears mapping of the .NET sub_type claim) gets 403. - persons_repo: re-add resolve_person_id_by_email_any_tenant (tenant-agnostic ROW_NUMBER() latest-observation pick; raw SQL per gears-rust#4239). - handlers: internal_person_by_email + InternalPersonResponse wire struct. - mod: register as a RAW axum route so it stays out of the generated OpenAPI, matching the .NET .ExcludeFromDescription(); auth + SecurityContext still come from the host pipeline like every other route. - test: lock the { value_type, value, insight_source_type, insight_source_id } wire shape the authenticator depends on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
Port GET /v1/subchart/{person_id} and GET /v1/subchart from the .NET
SubchartEndpoints / SubchartService / Sql.Subchart.cs (#348 / #344), plus the
visibility predicate from Sql.Visibility.cs::IsTargetInVisibleSet — the first
visibility gating in the Rust port (the read-side subordinates[] descent is
unguarded).
- infra/db/sql_named: bind_named helper — rewrites the .NET named @params
(which repeat on every recursion level) to positional `?` in occurrence
order, so the recursive-CTE SQL stays character-identical to the .NET source
while running on SeaORM. Unit-tested (repeats, terminators, bare @, unbound).
- infra/db/subchart_repo: is_target_in_visible_set (CanSee), get_subchart_flat,
get_forest_flat — raw WITH RECURSIVE + ROW_NUMBER() on the self-managed pool
(per gears-rust#4239); inline SQL comments stripped, rationale in Rust docs.
- domain/subchart: SubchartNode/SubchartResponse/SubchartForestResponse DTOs +
assemble_forest (flat→tree, O(N), ported from BuildTree). Unit-tested.
- api/subchart: get_forest + get_subchart handlers; require_caller gate (401);
depth (>=0) + valid_at (RFC-3339/naive/date-only → UTC, no-future) validation
matching the .NET binder. 404-on-deny hides existence. Unit-tested.
- api/gate: extract require_caller (baseline identified-caller gate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
Findings from a full-branch review (3 independent passes) verified against the .NET source: - person-roles last-admin lockout: the single guarded UPDATE's correlated COUNT is a snapshot read on MariaDB, so two concurrent revokes of different admins in a 2-admin tenant could both pass and leave zero admins. Restore the .NET behaviour: a RepeatableRead transaction that takes row-level write locks on every active admin in the tenant (SELECT ... FOR UPDATE) before the UPDATE. - reason length (<=500) was unvalidated on person-role create and on both revoke paths (person-roles + visibility); .NET rejects with 400 invalid_reason. Validate consistently. - subchart CanSee decoded SELECT EXISTS(...) as i64, the typed-EXISTS-scalar pattern roles_repo deliberately avoids; rewrite as a SELECT 1 ... LIMIT 1 presence probe so truthiness maps cleanly through SeaORM. - depth overflow saturated to i32::MAX (200) instead of 400; reject as invalid_depth to match the .NET int? binder. - person-role id validation reported the wrong field; emit per-field invalid_person_id / invalid_role_id. - negative ?limit= failed query deserialization (400); type it i64 and clamp to [1,500] (negative -> 1), matching the .NET int? clamp. Each fix carries a unit test. 50 tests pass; clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
…rows (#1755) Address the availability findings from a second review pass: - subchart depth is now clamped to the server's config.max_depth (and defaults to it when omitted) via effective_depth(), instead of passing an unbounded None to the UNION ALL subtree CTEs. Closes two levers: a caller omitting ?depth= to pull a whole large-tenant tree in one request, and cyclic org_chart data recursing until cte_max_recursion_depth (now a bounded partial tree, not a 500). The visibility gate's CTE is left as-is: it is UNION/distinct, so it self-terminates on cycles. - assemble_forest now warn-logs rows unreachable from any root (orphaned or cyclic) that get dropped from the response, so corrupted org data is visible in telemetry instead of a silently-shrunk tree. - drop the now-stale #![allow(dead_code)] on subchart_repo (all three query fns are called from the handlers). Tests: depth-cap, orphan-drop, and 2-cycle-terminates. 53 pass; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
…ution-domains-cutover
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe PR expands identity-resolution with shared authorization, internal email lookup, role and visibility management APIs, recursive subchart queries, hierarchical response assembly, canonical errors, repository support, and route registration. ChangesIdentity-resolution API expansion
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant Client
participant SubchartAPI
participant SubchartRepo
participant DomainAssembler
Client->>SubchartAPI: Request subchart with depth and valid_at
SubchartAPI->>SubchartRepo: Query visible hierarchy using recursive CTEs
SubchartRepo-->>SubchartAPI: Return flat subchart nodes
SubchartAPI->>DomainAssembler: Assemble forest or subtree
DomainAssembler-->>SubchartAPI: Return hierarchical response
SubchartAPI-->>Client: Return subchart JSON
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/backend/services/identity-resolution/src/infra/db/person_roles_repo.rs (1)
185-259: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winGuarded lock/update runs for every revoke, not only admin-role revokes — avoidable lock contention.
try_soft_delete_protecting_last_adminalways takes aFOR UPDATElock on every active-admin row in the tenant (Lines 194-204), even when the assignment being revoked isn't theadminrole. Since the caller (person_roles.rs::delete_person_role) already fetched the target row and knows itsrole_idbefore calling this function, non-admin revokes could skip the lock/guard path entirely and go straight to a plain soft-delete, avoiding contention with concurrent admin-roster changes in busy tenants.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/identity-resolution/src/infra/db/person_roles_repo.rs` around lines 185 - 259, The try_soft_delete_protecting_last_admin flow unnecessarily locks the tenant’s active-admin rows for non-admin revocations. Use the caller’s known target role_id to branch in delete_person_role: perform the guarded lock/count/update only for admin-role revokes, and use the existing plain soft-delete path for other roles; preserve the current last-admin protection and reason update behavior.src/backend/services/identity-resolution/src/api/handlers.rs (1)
95-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for the service-principal gate.
Only the wire-shape of
InternalPersonResponseis tested; the security-criticalctx.subject_type() != Some("service")→ 403 branch (Line 108) has no test asserting a non-service caller is rejected. Since this is a SERVICE-ONLY login-bootstrap endpoint, a regression here would be high-impact and silent.Also applies to: 367-393
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/identity-resolution/src/api/handlers.rs` around lines 95 - 132, The internal_person_by_email handler lacks coverage for rejecting non-service principals. Add a test that invokes the handler with a SecurityContext whose subject_type is not "service" and asserts it produces the canonical 403 permission-denied response, while preserving the existing successful response-shape test.src/backend/services/identity-resolution/src/api/gate.rs (1)
41-58: 🔒 Security & Privacy | 🔵 TrivialConsider logging denied admin-gate attempts.
Only the DB-error branch logs (
tracing::error!); the 403 "not admin" branch is silent. Since this gate protects every admin-only endpoint (roles, person-roles, visibility, persons-seed), a log/metric on denial would help detect repeated unauthorized probing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/identity-resolution/src/api/gate.rs` around lines 41 - 58, Add a warning log or metric in require_admin for the !is_admin denial path, including the caller and tenant context when available, while preserving the existing permission_denied response and DB-error logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/services/identity-resolution/src/api/roles.rs`:
- Around line 70-93: Trim req.name before passing it to role_name_valid,
roles_repo::get_by_name, and the subsequent role creation/storage flow. Use the
trimmed value consistently for duplicate detection and persistence while
preserving the existing validation and error behavior.
In `@src/backend/services/identity-resolution/src/api/visibility.rs`:
- Around line 109-120: Update the visibility request validation near the
existing viewer_person_id check to reject Some(viewed_person_id) when that UUID
is nil, using the same invalid-argument field-violation pattern and appropriate
viewed_person_id field name. Preserve None as the whole-tree visibility case and
leave reason_valid validation unchanged.
In `@src/backend/services/identity-resolution/src/infra/db/persons_repo.rs`:
- Around line 80-115: Add a tenant-agnostic composite index for the lookup
performed by resolve_person_id_by_email_any_tenant, covering value_type and
value_id and preferably the created_at, id, and person_id columns used for
ranking and selection. Define it in the persons table schema or migration using
the project’s existing index conventions.
---
Nitpick comments:
In `@src/backend/services/identity-resolution/src/api/gate.rs`:
- Around line 41-58: Add a warning log or metric in require_admin for the
!is_admin denial path, including the caller and tenant context when available,
while preserving the existing permission_denied response and DB-error logging.
In `@src/backend/services/identity-resolution/src/api/handlers.rs`:
- Around line 95-132: The internal_person_by_email handler lacks coverage for
rejecting non-service principals. Add a test that invokes the handler with a
SecurityContext whose subject_type is not "service" and asserts it produces the
canonical 403 permission-denied response, while preserving the existing
successful response-shape test.
In `@src/backend/services/identity-resolution/src/infra/db/person_roles_repo.rs`:
- Around line 185-259: The try_soft_delete_protecting_last_admin flow
unnecessarily locks the tenant’s active-admin rows for non-admin revocations.
Use the caller’s known target role_id to branch in delete_person_role: perform
the guarded lock/count/update only for admin-role revokes, and use the existing
plain soft-delete path for other roles; preserve the current last-admin
protection and reason update behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 082ea410-0ca6-4251-bbbf-20ffd4d35e7c
📒 Files selected for processing (18)
src/backend/services/identity-resolution/src/api/error.rssrc/backend/services/identity-resolution/src/api/gate.rssrc/backend/services/identity-resolution/src/api/handlers.rssrc/backend/services/identity-resolution/src/api/mod.rssrc/backend/services/identity-resolution/src/api/person_roles.rssrc/backend/services/identity-resolution/src/api/roles.rssrc/backend/services/identity-resolution/src/api/seed.rssrc/backend/services/identity-resolution/src/api/subchart.rssrc/backend/services/identity-resolution/src/api/visibility.rssrc/backend/services/identity-resolution/src/domain/mod.rssrc/backend/services/identity-resolution/src/domain/subchart.rssrc/backend/services/identity-resolution/src/infra/db/mod.rssrc/backend/services/identity-resolution/src/infra/db/person_roles_repo.rssrc/backend/services/identity-resolution/src/infra/db/persons_repo.rssrc/backend/services/identity-resolution/src/infra/db/roles_repo.rssrc/backend/services/identity-resolution/src/infra/db/sql_named.rssrc/backend/services/identity-resolution/src/infra/db/subchart_repo.rssrc/backend/services/identity-resolution/src/infra/db/visibility_repo.rs
| pub async fn resolve_person_id_by_email_any_tenant( | ||
| db: &DatabaseConnection, | ||
| email: &str, | ||
| ) -> anyhow::Result<Option<Uuid>> { | ||
| const SQL: &str = r" | ||
| WITH ranked AS ( | ||
| SELECT | ||
| person_id, | ||
| id, | ||
| ROW_NUMBER() OVER ( | ||
| PARTITION BY insight_tenant_id, insight_source_type, insight_source_id, value_type, value_id | ||
| ORDER BY created_at DESC, id DESC | ||
| ) AS rn, | ||
| created_at | ||
| FROM persons | ||
| WHERE value_type = 'email' | ||
| AND value_id = ? | ||
| ) | ||
| SELECT person_id | ||
| FROM ranked | ||
| WHERE rn = 1 | ||
| ORDER BY created_at DESC, id DESC | ||
| LIMIT 1 | ||
| "; | ||
|
|
||
| let stmt = | ||
| Statement::from_sql_and_values(DbBackend::MySql, SQL, [email.trim().to_owned().into()]); | ||
|
|
||
| match db.query_one(stmt).await? { | ||
| Some(row) => { | ||
| let bytes: Vec<u8> = row.try_get("", "person_id")?; | ||
| Ok(Some(Uuid::from_slice(&bytes)?)) | ||
| } | ||
| None => Ok(None), | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for migration/DDL defining the `persons` table and its indexes.
fd -i migration
rg -n --type sql -i -A3 -B3 'CREATE TABLE .*persons|CREATE (UNIQUE )?INDEX'
rg -n -i -A5 'persons' -g '*migration*' -g '*.sql'Repository: constructorfabric/insight
Length of output: 565
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## candidates"
git ls-files | rg -n 'persons|migration|schema|migrations|migrat|db' | sed -n '1,200p'
echo "## persons references in tracked files"
rg -n --hidden -i '\bpersons\b' | sed -n '1,240p'
echo "## SQL DDL/INDEX references"
rg -n --hidden -i -C 2 'CREATE TABLE|CREATE UNIQUE INDEX|CREATE INDEX|INDEX|UNIQUE KEY|KEY|ALTER TABLE' --glob '*.sql' --glob '*.cs' --glob '*.rs' --glob '*.rs.inu' --glob '*.sql.*' --glob '*.fs' --glob '*.fsx' | sed -n '1,260p'Repository: constructorfabric/insight
Length of output: 16231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## persons migration SQL"
sed -n '1,240p' src/backend/services/identity/src/Insight.Identity.Infrastructure/Migrations/001_persons.sql
echo "---"
sed -n '1,240p' src/backend/services/identity/src/Insight.Identity.Infrastructure/Migrations/004_persons_relax_constraints.sql
echo "## entity/model"
sed -n '1,220p' src/backend/services/identity-resolution/src/infra/db/entities/persons.rs
echo "## local repository references"
rg -n -i '\bpersons\b|value_type|value_id|insight_tenant_id|create_unique_index|create_index|person_id' src/backend/services/identity src/backend/services/identity-resolution src/backend/services/analytics --glob '*.sql' --glob '*.rs' --glob '*.cs' | sed -n '1,360p'
echo "## migrations index DDL"
rg -n --type sql -C 3 'person|idx|index|unique|persons' src/backend/services/identity/src/Insight.Identity.Infrastructure/Migrations src/backend/services/identity-resolution src/backend/services/analytics/src/migration --glob '*.sql' --glob '*.rs' | sed -n '1,420p'Repository: constructorfabric/insight
Length of output: 50382
Add a matching non-tenant index for tenant-agnostic email resolution.
persons only has tenant-prefixed indexes (insight_tenant_id, value_type, value_id) plus value columns, so resolve_person_id_by_email_any_tenant’s CTE cannot use those indexes for its value_type = 'email' AND value_id = ? filter. Add an index covering this lookup pattern, e.g. (value_type, value_id, created_at, id, person_id) or tenant-agnostic indexes on the value columns used in the CTE/ORDER BY.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/services/identity-resolution/src/infra/db/persons_repo.rs` around
lines 80 - 115, Add a tenant-agnostic composite index for the lookup performed
by resolve_person_id_by_email_any_tenant, covering value_type and value_id and
preferably the created_at, id, and person_id columns used for ranking and
selection. Define it in the persons table schema or migration using the
project’s existing index conventions.
- persons: add migration 013 idx_value_id_any_tenant (value_type, value_id) —
the tenant-agnostic email lookup (GET /internal/persons/by-email) could not use
the tenant-prefixed idx_value_id and full-scanned persons on every login.
- person-roles revoke: only admin-role revokes take the tenant-wide FOR UPDATE
last-admin lock now; non-admin revokes use a plain (tenant-scoped) soft_delete,
avoiding lock contention (the guard is a no-op for non-admin roles anyway).
- roles: trim the name before validate/store/duplicate-check so whitespace-only
variants (" Admin " vs "Admin") don't accumulate in the global catalogue.
- visibility: reject a present nil viewed_person_id (400) — only an ABSENT target
means whole-tree visibility.
- gate: extract require_service (service-principal 403 gate) out of the internal
handler and unit-test it; warn-log denied admin-gate attempts (caller + tenant).
54 tests pass; clippy -D warnings clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
#1755) Security parity fix (external review): the .NET service gates /v1/profiles through VisibilityService.CanSeeAsync and masks a deny as 404; the Rust port returned the assembled profile without any caller-visibility check, so any authenticated tenant user could resolve profiles of people they cannot see. Restore parity in resolve_profile: require an identified caller (401 otherwise), then check subchart_repo::is_target_in_visible_set(caller → target) and mask a deny as 404 (same shape as not-found, so existence does not leak). Reuses the visible_set predicate already used by the subchart endpoints. Also `cargo fmt` (the prior commit left person_roles.rs / visibility.rs unformatted, failing the fmt CI gate). 54 tests pass; clippy -D warnings clean; fmt --check clean. Deny-as-404 needs an integration test, tracked with the live-verification work (#1753). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/backend/services/identity-resolution/src/api/handlers.rs (1)
135-152: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject ambiguous email matches before resolving identities.
resolve_person_id_by_email_any_tenantranks one latest row per(tenant, source, value_id)and then returns a single row across tenants. Duplicate normalized emails across tenants can therefore return the biggest/newest source-id match and avoid signaling ambiguity; detect multiple matched persons before returning oneperson_id.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/identity-resolution/src/api/handlers.rs` around lines 135 - 152, Update the by-email lookup around resolve_person_id_by_email_any_tenant to retrieve and validate all matching persons before selecting an identity, rejecting the request when multiple distinct person matches exist rather than accepting the ranked result. Preserve the existing internal error mapping, not-found response, and successful InternalPersonResponse behavior for exactly one match.
🧹 Nitpick comments (1)
src/backend/services/identity/src/Insight.Identity.Infrastructure/Migrations/013_persons_email_any_tenant_idx.sql (1)
8-9: 🚀 Performance & Scalability | 🔵 TrivialVerify online-DDL behavior before applying this index to production.
personsis likely a hot table, and index creation can acquire metadata or table locks depending on the database version and engine. Confirm the target migration strategy supports online DDL or schedule this migration outside peak traffic; Squawk’s PostgreSQL-styleCONCURRENTLYsuggestion should not be copied blindly to MariaDB/MySQL.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/identity/src/Insight.Identity.Infrastructure/Migrations/013_persons_email_any_tenant_idx.sql` around lines 8 - 9, Verify the production database engine/version and migration strategy for the idx_value_id_any_tenant index before applying it. Ensure CREATE INDEX in migration 013_persons_email_any_tenant_idx.sql uses the supported online-DDL approach for MariaDB/MySQL, or explicitly schedule it outside peak traffic; do not add PostgreSQL-specific CONCURRENTLY syntax.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/services/identity-resolution/src/api/handlers.rs`:
- Around line 41-71: The visibility check currently runs only after ambiguity
handling, leaking hidden matches and rejecting a uniquely visible candidate.
Update the `resolve_person_ids` flow to filter all candidate IDs through
`subchart_repo::is_target_in_visible_set` before matching on the results,
preserving the existing error mapping and 404 behavior for no visible
candidates; then perform the one-result versus ambiguity decision on the
filtered set.
---
Outside diff comments:
In `@src/backend/services/identity-resolution/src/api/handlers.rs`:
- Around line 135-152: Update the by-email lookup around
resolve_person_id_by_email_any_tenant to retrieve and validate all matching
persons before selecting an identity, rejecting the request when multiple
distinct person matches exist rather than accepting the ranked result. Preserve
the existing internal error mapping, not-found response, and successful
InternalPersonResponse behavior for exactly one match.
---
Nitpick comments:
In
`@src/backend/services/identity/src/Insight.Identity.Infrastructure/Migrations/013_persons_email_any_tenant_idx.sql`:
- Around line 8-9: Verify the production database engine/version and migration
strategy for the idx_value_id_any_tenant index before applying it. Ensure CREATE
INDEX in migration 013_persons_email_any_tenant_idx.sql uses the supported
online-DDL approach for MariaDB/MySQL, or explicitly schedule it outside peak
traffic; do not add PostgreSQL-specific CONCURRENTLY syntax.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f699fc58-c90d-4569-8aee-bbe505c73c8a
📒 Files selected for processing (9)
src/backend/services/identity-resolution/src/api/gate.rssrc/backend/services/identity-resolution/src/api/handlers.rssrc/backend/services/identity-resolution/src/api/person_roles.rssrc/backend/services/identity-resolution/src/api/roles.rssrc/backend/services/identity-resolution/src/api/subchart.rssrc/backend/services/identity-resolution/src/api/visibility.rssrc/backend/services/identity-resolution/src/infra/db/person_roles_repo.rssrc/backend/services/identity-resolution/src/infra/db/subchart_repo.rssrc/backend/services/identity/src/Insight.Identity.Infrastructure/Migrations/013_persons_email_any_tenant_idx.sql
🚧 Files skipped from review as they are similar to previous changes (7)
- src/backend/services/identity-resolution/src/infra/db/subchart_repo.rs
- src/backend/services/identity-resolution/src/infra/db/person_roles_repo.rs
- src/backend/services/identity-resolution/src/api/gate.rs
- src/backend/services/identity-resolution/src/api/roles.rs
- src/backend/services/identity-resolution/src/api/person_roles.rs
- src/backend/services/identity-resolution/src/api/subchart.rs
- src/backend/services/identity-resolution/src/api/visibility.rs
…ole 409 (#1755) External review (Request changes) — the code-level items: - valid_from on person-roles / visibility used sea_orm::DateTime (= chrono::NaiveDateTime), whose serde parser rejects the Z / offset forms the .NET `format: date-time` contract accepts, so a normal client value like 2026-07-23T10:00:00Z would 400 before the handler. Add a shared flexible deserializer (RFC-3339 with Z/offset, zone-less, or date-only -> naive-UTC). - role create: a concurrent same-name POST that loses the UNIQUE(name) race after the friendly pre-check surfaced as a generic 500; re-read on insert error and map the duplicate to already_exists (409). Deferred to the cutover PR (#1753), with reasons: authenticator empty-tenant service-token contract (cross-service); OpenAPI path/query param declarations (pairs with wiring the Rust OpenAPI drift gate); person_roles->roles FK to close the delete-vs-grant race (migration); CI registration + MariaDB integration / parity suite. The subchart "synthetic root for unknown id" is faithful .NET parity (the anchor is literal in Sql.Subchart.cs) — left as-is, documented. 55 tests pass; clippy -D warnings clean; fmt --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
) Code-quality review — must-fixes: - main.rs did not link `oidc_authn_plugin` (only 9 of the 10 system gears the analytics host links). Gears register via `inventory` at link time, so the unreferenced crate is dropped and never registers — auth could be silently absent at runtime. Add `use oidc_authn_plugin as _;` (runtime "no JWT -> 401" check tracked with live verification, #1753). - Drop 8 stale `#![allow(dead_code)]` (kept only entities/, codegen) — the crate builds clean without them, so they were only masking future dead code. - De-stale the lies: main.rs module doc / clap `about` / "auth disabled" comment now reflect reality (auth ON, full surface); handlers.rs ambiguous "flagged for review" -> statement; persons_repo `parent_person_id IS NOT NULL` comment was wrong (.NET's CurrentParentsForChild has the same predicate) -> provenance. Minor: include the resolved ids in the ambiguous-profile 409 detail; nil-tenant guard in require_caller (explicit 400 vs a silent nil-tenant query); seed `?limit` u64->i64 + clamp (parity with sibling routes); remove the dead `account_person_map` entity; README auth/endpoint wording. 55 tests pass; clippy -D warnings clean; fmt --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/backend/services/identity-resolution/src/api/datetime.rs (1)
54-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest the serde adapter at the deserialization boundary.
parse_flexibleis covered, butdeserialize_optadds distinct behavior for missing, empty, and invalid values. Add a smallserde_json-backed test matrix for these cases.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/services/identity-resolution/src/api/datetime.rs` around lines 54 - 88, The existing tests only cover parse_flexible and not the deserialize_opt serde boundary. Add a concise serde_json-backed test matrix in the tests module that exercises missing, empty, valid, and invalid values, asserting the expected optional datetime results and deserialization errors for deserialize_opt.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/backend/services/identity-resolution/src/api/datetime.rs`:
- Around line 54-88: The existing tests only cover parse_flexible and not the
deserialize_opt serde boundary. Add a concise serde_json-backed test matrix in
the tests module that exercises missing, empty, valid, and invalid values,
asserting the expected optional datetime results and deserialization errors for
deserialize_opt.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6096952e-4625-48bf-b033-619e5468e85f
📒 Files selected for processing (21)
src/backend/services/identity-resolution/README.mdsrc/backend/services/identity-resolution/src/api/datetime.rssrc/backend/services/identity-resolution/src/api/gate.rssrc/backend/services/identity-resolution/src/api/handlers.rssrc/backend/services/identity-resolution/src/api/mod.rssrc/backend/services/identity-resolution/src/api/person_roles.rssrc/backend/services/identity-resolution/src/api/roles.rssrc/backend/services/identity-resolution/src/api/seed.rssrc/backend/services/identity-resolution/src/api/visibility.rssrc/backend/services/identity-resolution/src/domain/seed.rssrc/backend/services/identity-resolution/src/domain/seed_service.rssrc/backend/services/identity-resolution/src/infra/db/entities/account_person_map.rssrc/backend/services/identity-resolution/src/infra/db/entities/mod.rssrc/backend/services/identity-resolution/src/infra/db/ops_repo.rssrc/backend/services/identity-resolution/src/infra/db/person_roles_repo.rssrc/backend/services/identity-resolution/src/infra/db/persons_repo.rssrc/backend/services/identity-resolution/src/infra/db/roles_repo.rssrc/backend/services/identity-resolution/src/infra/db/seed_repo.rssrc/backend/services/identity-resolution/src/infra/db/visibility_repo.rssrc/backend/services/identity-resolution/src/infra/identity_inputs.rssrc/backend/services/identity-resolution/src/main.rs
💤 Files with no reviewable changes (10)
- src/backend/services/identity-resolution/src/domain/seed_service.rs
- src/backend/services/identity-resolution/src/infra/db/entities/account_person_map.rs
- src/backend/services/identity-resolution/src/infra/db/entities/mod.rs
- src/backend/services/identity-resolution/src/infra/identity_inputs.rs
- src/backend/services/identity-resolution/src/infra/db/seed_repo.rs
- src/backend/services/identity-resolution/src/domain/seed.rs
- src/backend/services/identity-resolution/src/infra/db/ops_repo.rs
- src/backend/services/identity-resolution/src/infra/db/visibility_repo.rs
- src/backend/services/identity-resolution/src/infra/db/person_roles_repo.rs
- src/backend/services/identity-resolution/src/infra/db/roles_repo.rs
🚧 Files skipped from review as they are similar to previous changes (7)
- src/backend/services/identity-resolution/src/infra/db/persons_repo.rs
- src/backend/services/identity-resolution/src/api/gate.rs
- src/backend/services/identity-resolution/src/api/visibility.rs
- src/backend/services/identity-resolution/src/api/mod.rs
- src/backend/services/identity-resolution/src/api/handlers.rs
- src/backend/services/identity-resolution/src/api/roles.rs
- src/backend/services/identity-resolution/src/api/person_roles.rs
cyberantonz
left a comment
There was a problem hiding this comment.
Some finding over code
… date parsing (#1755) Address remaining review findings on #1862: - resolve_profile: apply the caller's visibility gate to ALL candidate person_ids before deciding not-found / resolved / ambiguous, not only to the single-match branch. Previously a hidden candidate could leak its existence through the AMBIGUOUS_PROFILE id list, and a uniquely visible candidate among several matches was misreported as ambiguous instead of being resolved (CodeRabbit, Major). - subchart::parse_valid_at duplicated datetime::parse_flexible's RFC-3339 / zone-less / date-only parsing verbatim; drop it and call the shared helper instead (cyberantonz). - datetime::deserialize_opt had no test at the serde boundary (missing/ null/empty/blank/valid/invalid); add a matrix test (CodeRabbit, nitpick). 55 tests pass; clippy -D warnings clean; fmt --check clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ution-domains-cutover
What
Ports the remaining .NET
identityendpoints to the Rustidentity-resolutionservice, completing code-level parity with the .NET service. Stacked on the
read (#1745) and write-seed (#1825) work, both already in
main.Endpoints added in this PR:
POST/GET/DELETE /v1/roles(admin-gated; hard-delete withan atomic in-use guard).
POST/GET/DELETE /v1/person-roles(grant/list/revoke;last-admin lockout guard enforced in a
RepeatableReadtxn withFOR UPDATE).POST/GET/DELETE /v1/visibility(create/list/revoke grants).GET /v1/subchart(forest) andGET /v1/subchart/{person_id}(single), recursive-CTE traversals with the first visibility gating in the port.
GET /internal/persons/by-email/{email}, service-only(
sub_type=service), for the authenticator's login bootstrap.require_admin/require_caller), and abind_namedhelper that expands the .NET named
@paramsto SeaORM positional?so therecursive CTEs stay character-identical to the .NET source.
Verification
clippy -D warningsclean.last-admin lockout race (txn +
FOR UPDATE),reasonlength validation,driver-agnostic visibility probe, depth/field/limit hardening, bounded subchart
depth + dropped-row telemetry.
POST /v1/profiles,forwards the user JWT) is compatible and now correctly visibility-gated. The
authenticator's internal lookup is contract-matched but is currently blocked
at the OIDC layer by an empty-tenant service token — see Known limitations.
POST /v1/profilesvisibility gate restored (was missing on the read path):require_caller+is_target_in_visible_set, deny→404, parity with the .NETVisibilityService.CanSeeAsync.(last-admin lock, reason validation, RFC-3339
valid_from, concurrent-dup 409,bounded subchart depth, per-field validation, service-gate test, …).
Intentional divergences from .NET (unchanged from #1745/#1825)
gts.cf.insight.identity_resolution.*(RFC-9457) insteadof
urn:insight:error:*.aborted;role-in-use / last-admin → 400
failed_precondition.next_cursordropped from list responses (was always null)..ExcludeFromDescription()).X-Insight-Tenant-Id/X-Insight-Person-Idheaders are no longer read.dbcapability) — seetoolkit-db: no raw-SQL path (DbConn/DbTx) — window functions, recursive CTEs & atomic conditional DML can't be expressed, forcing a bespoke SeaORM pool gears-rust#4239.
depthon subchart is clamped to the server'smax_depth(and defaults to itwhen omitted) — a deliberate bound the .NET endpoint lacked, so a caller cannot
force an unbounded org-tree traversal.
GET /v1/persons/{email}is intentionally not carried: noconsumer calls it, and the redesign removes it.
Known limitations — NOT yet swap-ready (do not switch traffic on merge)
This PR is code-only; the following block a production cutover and are
tracked for the next PR (#1753):
tenant_id; the gears OIDC plugin parsestenant_idas a UUID and 401s beforethe handler, so
GET /internal/persons/by-email/{email}is unreachable. Needsa tenantless service-token path (or a sentinel tenant) — a cross-service change.
not run DbUp or seed the first admin role. Migrations (incl.
013) still livein the .NET project. Clean-env bring-up + first-admin bootstrap are unresolved.
8083whileconsumers expect
identity:8082; env prefix changed (IDENTITY__→APP__gears__identity-resolution__config__); OIDC full-auth config/CA notmounted.
identity-resolutionis not registered in CI(
scripts/ci/components.py); the OpenAPI drift gate still targets .NET; thereis no HTTP+MariaDB integration / parity suite. OpenAPI path/query params for the
new routes are not yet declared (pairs with wiring the Rust drift gate).
Known parity quirks (faithful to .NET, documented)
GET /v1/subchart/{person_id}synthesizes a null-valued root for an unknown idinstead of 404 — the anchor is literal in
Sql.Subchart.cs; unchanged here.GET /v1/persons/{email}(deprecated) intentionally dropped — no consumer.Deferred polish
Helper de-duplication, a domain-owned subchart DTO, redacted
Debugon theClickHouse password.
Part of #1755. Merging this does not enable the cutover — it lands the code;
the swap happens in #1753 after the blockers above are closed.
🤖 Generated with Claude Code
Summary by CodeRabbit
valid_atfiltering).limithandling and strengthened protection against revoking the last active administrator.