Skip to content

feat(account-management): tenant-metadata + AM subtree clamp + observability ports#16

Open
diffora wants to merge 2 commits into
am/04-conversion-servicefrom
am/05-tenant-metadata
Open

feat(account-management): tenant-metadata + AM subtree clamp + observability ports#16
diffora wants to merge 2 commits into
am/04-conversion-servicefrom
am/05-tenant-metadata

Conversation

@diffora

@diffora diffora commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Tenant-metadata feature end-to-end — SDK contracts, domain service + repo with own-first / barrier-stop / suspended-skip walk-up, GTS-backed metadata schema registry, migration m0005, PG integration tests. User-groups cascade hook fires on tenant hard-delete before the IdP call.
  • InTenantSubtree wiring for AM tenant CRUD (cyberware-rust#1813) — tenants entity declares resource_col = "id", TenantService::authorize flips to require_constraints(true) with OWNER_TENANT_ID + RESOURCE_ID, and the compiled AccessScope is forwarded into read_tenant / list_children / update_tenant / soft_delete. PolicyEnforcer advertises Capability::TenantHierarchy so the PDP returns native InTenantSubtree rather than pre-resolved In.
  • Observability ports + OTel adapter — new domain/ports/metrics.rs with seven segregated trait families (Bootstrap, Conversion, Dependency, Integrity, Metadata, Tenant, Storage) and a typed label-value taxonomy (every type annotated #[domain_model] per DE0309). New infra/metrics.rs holds AmMetricsMeter (12 counters / 4 gauges / 1 histogram) implementing every port trait plus the transitional MetricsFacadeBridge. domain/metrics.rs uses LazyLock<ArcSwap<Option<BridgeArc>>> so install_facade_bridge is replaceable (meter-provider hot-swap in tests), and module.rs gates the install on modkit::telemetry::metrics_provider_installed() so the NoopMeterProvider posture stays zero-cost on hot paths. New mark_metrics_provider_installed() escape hatch for embedders that wire OTel outside modkit bootstrap (used by api-gateway/tests/http_metrics_tests.rs).
  • Forward-compatconversion_requests entity declares tenant_col = "tenant_id"; service still passes allow_all to the conversion repo because dual-consent flows need barrier penetration on self-managed children. MetadataService walk-up uses allow_all for ancestor reads (start tenant is PEP-authorized; ancestors are outside the caller's subtree by definition of the new clamp).

Authorization story

Before After
tenants Scopable(no_*); service discarded compiled scope; repo always saw allow_all tenants Scopable(resource_col="id"); service forwards scope; secure-extension materialises tenants.id IN (SELECT descendant_id FROM tenant_closure WHERE ancestor_id = :root AND barrier = 0) JOIN
Single-layer authz: service-level PDP gate only Defence-in-depth: PDP gate + DB-level subtree clamp; CompileFailed → CrossTenantDenied (HTTP 403) on missing-policy
Latent property-resolution mismatch with tr-authz-plugin masked by scope discard Mismatch exposed; deployment requires PDP/plugin to emit InTenantSubtree(RESOURCE_ID, …) or for the consumer to provide its own plugin

Observability story

Before After
Stringly-typed emit_metric / emit_gauge_value / emit_histogram_value no-ops at call sites Same call sites; calls now dispatch through a typed MetricsFacadeBridge onto OTel instruments per family constant
No typed labels — &str literals at every emit site Closed-set enums / sealed &'static str newtypes; From<&Failure> bridges delegate to SDK as_metric_label() — no duplicated variant→string mapping
OnceLock bridge (first-wins forever) — stale instruments after meter-provider swap LazyLock<ArcSwap<Option<BridgeArc>>> — replace semantics; tests that hot-swap providers re-install bridges atomically
Bridge installed unconditionally even with opentelemetry.metrics.enabled = falseKeyValue allocations on every emit Install gated on modkit::telemetry::metrics_provider_installed(); NoopMeterProvider posture keeps emit_* on silent no-op fast path

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo dylint --all (workspace, includes DE0309 + DE1101 enforcement on the new ports file)
  • cargo test -p cyberware-account-management --lib — 535 passed (up from 514; +21 covers metrics tests + port-trait taxonomy + observability bridge)
  • cargo test -p cyberware-api-gateway --test http_metrics_tests — 6/6 (exercises mark_metrics_provider_installed escape hatch)
  • Integration: conversion_integration 10/10, lifecycle_integration 12/12, metadata_integration 5/5, repair_integration 11/11
  • Manual PG run: cargo test -p cyberware-account-management --test conversion_integration_pg --test metadata_integration_pg (requires PG)

Deployment note

AM now advertises Capability::TenantHierarchy and resolves only RESOURCE_ID on the tenants entity. Production deployment requires the PDP / AuthZ plugin chain to emit InTenantSubtree(RESOURCE_ID, …) (or a future plugin that respects Capability::TenantHierarchy); the in-tree tr-authz-plugin currently emits In(OWNER_TENANT_ID, …) which fails closed at the new entity boundary. Each vendor will ship its own plugin variant per the AM SDK contract.

Notes for reviewers

  • The branch was rebased into a single squashed commit; full algorithm-level rationale lives in the commit body.
  • See docs/pr-review/modkit-rust-review.md + modkit-framework-compliance-review.md for the review checklist.
  • Negative cross-subtree tests use constraint_bearing_enforcer(child) against an out-of-subtree target to assert NotFound at the secure-extension layer.
  • The emit_* facade + MetricsFacadeBridge are transitional — per-family call-site migration onto the typed ports lands in follow-up PRs; the bridge is removed once every legacy call site has moved.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added tenant metadata management system supporting create, read, update, and delete operations with configurable inheritance policies and schema validation.
    • Introduced user groups functionality with automatic type registration and cascade cleanup during tenant deletion.
  • Infrastructure Updates

    • Implemented OpenTelemetry-backed metrics for enhanced observability.
    • Enhanced authorization with subtree-based access control for improved security isolation.
    • Added database indexes to optimize metadata query performance.

Review Change Stack

Phase 1 dual-consent conversion lifecycle and IdP user-operations
contract. Domain + storage land here; REST handlers in #1813.

GTS-runtime validation (gts.cf.core.am.{user,tenant,tenant_metadata}.v1~)
replaces hardcoded length checks; bootstrap saga uses the same gate.

SDK: IdP failure enums now implement Display + Error + detail();
ListChildrenQuery.DEFAULT_TOP = 50 mirrors UserPagination so REST
queries that omit `top` no longer 400.

Signed-off-by: Diffora <ddiffora@gmail.com>
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b3bd04a-965f-4473-ba83-5f2d40d138ca

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch am/05-tenant-metadata

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
modules/system/account-management/account-management/src/domain/conversion/service.rs (1)

1458-1464: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the same nil-actor guard in approve as other transition methods.

request_conversion, cancel, and reject reject Uuid::nil() actor IDs, but approve does not. This allows persisting/logging an invalid approver identity and breaks audit consistency.

🤖 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
`@modules/system/account-management/account-management/src/domain/conversion/service.rs`
around lines 1458 - 1464, Add the same nil-actor guard to the start of the
approve method: check if approver_uuid == Uuid::nil() and return the same
DomainError used by request_conversion/cancel/reject (i.e., the
invalid/empty-actor error) instead of proceeding; mirror the exact guard pattern
and error return used in request_conversion, cancel and reject so approve (in
the approve(&self, scope: &AccessScope, request_id: Uuid, caller:
ConversionCaller, approver_uuid: Uuid) -> ...) enforces the same nil-UUID
validation and preserves audit consistency.
modules/system/account-management/account-management/src/domain/conversion/repo.rs (1)

85-101: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update scope contract doc to match the actual conversion_requests entity declaration.

The doc at lines 85–86 states Scopable(no_tenant, no_resource, no_owner, no_type), but the entity declares tenant_col = "tenant_id". Replace no_tenant with tenant_col = "tenant_id" to align the contract with the current entity definition and prevent misleading assumptions in authz follow-ups.

🤖 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
`@modules/system/account-management/account-management/src/domain/conversion/repo.rs`
around lines 85 - 101, Update the scope contract comment for the
conversion_requests entity to reflect its current declaration: replace the
incorrect `Scopable(no_tenant, no_resource, no_owner, no_type)` wording with an
entry that shows the tenant column (e.g., `tenant_col = "tenant_id"`) so the
comment matches the actual entity declaration; locate the comment block
referencing `Scopable` and `conversion_requests` and change `no_tenant` to
`tenant_col = "tenant_id"` while keeping the rest of the explanation about scope
behavior and callers using `AccessScope::allow_all` intact.
🧹 Nitpick comments (3)
modules/system/account-management/account-management/tests/metadata_integration_pg.rs (1)

193-204: 💤 Low value

Consider parameterized queries or dedicated DDL helpers for raw SQL.

Lines 193–195 and 200 construct DELETE statements using string interpolation (format!). While UUIDs are safe from SQL injection, this pattern:

  • Bypasses the safety of parameterized queries
  • Could be copied to sites handling user-controlled strings
  • May not be the idiomatic approach in the codebase

If the testcontainer DDL connection supports parameterized statements, prefer that; otherwise, add a comment noting why raw interpolation is acceptable here (test-only, UUID values).

🤖 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
`@modules/system/account-management/account-management/tests/metadata_integration_pg.rs`
around lines 193 - 204, The test builds raw DELETE SQL via format! into the
variable raw and executes it with h.ddl_conn.execute_unprepared which bypasses
parameterized queries; update the calls to use the DDL connection's
parameterized execution API (instead of building the string) so the DELETE
statements for tenant_closure and tenants pass the target UUID as a bind
parameter, or if the DDL test client truly lacks parameter support, add a short
comment above these lines referencing execute_unprepared and explaining why
string interpolation is safe here (test-only and UUID-only) and link to the
test-only justification.
modules/system/account-management/account-management/src/domain/user_groups/cascade.rs (1)

322-351: ⚡ Quick win

Emit dependency-health metrics for membership list/remove failures

Line 322-Line 351 returns retryable on RG failures/timeouts but skips AM_DEPENDENCY_HEALTH emission, unlike the rest of the cascade path. Add counters here to keep failure attribution complete.

Proposed fix
         let page = match tokio::time::timeout(CASCADE_TIMEOUT, client.list_memberships(ctx, &query))
             .await
         {
-            Err(_) => return Err(retryable("timeout listing memberships")),
+            Err(_) => {
+                emit_metric(
+                    AM_DEPENDENCY_HEALTH,
+                    MetricKind::Counter,
+                    &[("target", "resource_group"), ("op", "cascade_list_memberships"), ("outcome", "timeout")],
+                );
+                return Err(retryable("timeout listing memberships"));
+            }
             Ok(Err(e)) => {
+                emit_metric(
+                    AM_DEPENDENCY_HEALTH,
+                    MetricKind::Counter,
+                    &[("target", "resource_group"), ("op", "cascade_list_memberships"), ("outcome", "error")],
+                );
                 return Err(retryable(format!(
                     "failed to list memberships for group {group_id}: {e}"
                 )));
             }
             Ok(Ok(p)) => p,
@@
                 Err(_) => {
+                    emit_metric(
+                        AM_DEPENDENCY_HEALTH,
+                        MetricKind::Counter,
+                        &[("target", "resource_group"), ("op", "cascade_remove_membership"), ("outcome", "timeout")],
+                    );
                     return Err(retryable(format!(
                         "timeout removing membership from group {group_id}"
                     )));
                 }
                 #[allow(clippy::match_same_arms)]
                 Ok(Err(ResourceGroupError::NotFound { .. })) => {}
                 Ok(Err(e)) => {
+                    emit_metric(
+                        AM_DEPENDENCY_HEALTH,
+                        MetricKind::Counter,
+                        &[("target", "resource_group"), ("op", "cascade_remove_membership"), ("outcome", "error")],
+                    );
                     return Err(retryable(format!(
                         "failed to remove membership from group {group_id}: {e}"
                     )));
                 }
🤖 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
`@modules/system/account-management/account-management/src/domain/user_groups/cascade.rs`
around lines 322 - 351, The code currently returns retryable errors inside the
CASCADE_TIMEOUT blocks for client.list_memberships and client.remove_membership
without emitting AM_DEPENDENCY_HEALTH counters; update the error paths in
cascade.rs around the match handling of tokio::time::timeout for
client.list_memberships (the Err(_) timeout branch and the Ok(Err(e)) branch)
and for client.remove_membership (the Err(_) timeout branch and the Ok(Err(e))
non-NotFound branch) to increment the AM_DEPENDENCY_HEALTH metric before
returning retryable, using a consistent label set (e.g.,
dependency="resource_group", operation="list_memberships" or
"remove_membership", status="timeout" or status="error") so failures are
attributed the same way as other cascade paths.
modules/system/account-management/account-management/src/domain/metadata/service.rs (1)

443-478: ⚡ Quick win

delete_for_tenant surfaces MetadataEntryNotFound with the internal schema_uuid, breaking error-shape parity with get_for_tenant.

The repo's delete_for_tenant builds the error from schema_uuid (lines 312-315 of infra/storage/repo_impl/metadata.rs), while the service's get_for_tenant builds the same DomainError::MetadataEntryNotFound from the public chained schema_id (lines 292-295 here). Forwarding the repo error verbatim means clients hitting the distinct-404 contract see two different entry / detail shapes for the same logical condition depending on whether they GET or DELETE, which the future REST handler (cyberfabric-core#1813) and any audit aggregator keyed on entry will have to special-case.

Since the public schema_id is already in scope at the call site, a small .map_err keeps the contract aligned without re-introducing a service-side existence probe.

♻️ Remap the repo's `MetadataEntryNotFound` to use the public `schema_id`
-        self.metadata_repo
-            .delete_for_tenant(scope, tenant_id, schema_uuid)
-            .await?;
+        self.metadata_repo
+            .delete_for_tenant(scope, tenant_id, schema_uuid)
+            .await
+            .map_err(|e| match e {
+                DomainError::MetadataEntryNotFound { .. } => {
+                    DomainError::MetadataEntryNotFound {
+                        detail: format!(
+                            "no metadata entry for tenant {tenant_id} at schema {schema_id}"
+                        ),
+                        entry: format!("({tenant_id}, {schema_id})"),
+                    }
+                }
+                other => other,
+            })?;
🤖 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
`@modules/system/account-management/account-management/src/domain/metadata/service.rs`
around lines 443 - 478, The delete_for_tenant service forwards the repo error
verbatim which embeds the internal schema_uuid in
DomainError::MetadataEntryNotFound; change the call to
metadata_repo.delete_for_tenant(...) in delete_for_tenant to map_err and, if the
error is DomainError::MetadataEntryNotFound, rebuild/convert it so the
entry/detail use the public schema_id (the existing schema_id variable) instead
of schema_uuid, otherwise propagate the original error; keep the rest of the
function (resolve_active_tenant, resolve_inheritance_policy, derive_schema_uuid,
logging) unchanged.
🤖 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
`@modules/system/account-management/account-management/src/domain/conversion/service.rs`:
- Around line 1409-1412: The debug-only assertion around
scope.is_unconstrained() (seen in soft_delete_resolved and the similar check at
the later block) must be converted into a runtime guard that returns an error
when the scope is narrowed; replace the debug_assert! with an if
!scope.is_unconstrained() { return Err(/* appropriate domain/error type */) }
pattern so callers receive a hard failure in production rather than proceeding
with global sweeps; ensure you use the function's existing error/result type and
include a clear message like "callers MUST pass AccessScope::allow_all()" in the
returned error.

In
`@modules/system/account-management/account-management/src/domain/user_groups/registration.rs`:
- Around line 138-144: The current TypeAlreadyExists race path in
registration.rs swallows all create races and returns
RegistrationOutcome::AlreadyPresent even if the concurrent creator registered
different traits; update the Err(ResourceGroupError::TypeAlreadyExists { .. })
branch to re-read the resource group type via get_type (the same call used
elsewhere) and compare its traits to the expected traits you intended to create;
if traits match return Ok(RegistrationOutcome::AlreadyPresent), if they differ
return an error (or propagate a new validation error) so init fails rather than
proceeding with incompatible traits (reference the create_type call, the
get_type call, ResourceGroupError::TypeAlreadyExists, and
RegistrationOutcome::AlreadyPresent to locate the code to change).

In
`@modules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs`:
- Around line 101-123: The match on effective.get(INHERITANCE_POLICY_TRAIT)
currently warns for every non-INHERIT_POLICY_TOKEN value (including the valid
default override-only); change the logic to explicitly treat the documented
override value as a silent, valid case and only emit tracing::warn! for truly
unknown or malformed values. Concretely: in the branch handling
effective.get(INHERITANCE_POLICY_TRAIT) check for Some(v) if v.as_str() ==
Some(INHERIT_POLICY_TOKEN) => InheritancePolicy::Inherit; if Some(v) matches the
documented override token (use the project's override token constant or the
literal "override_only") => InheritancePolicy::OverrideOnly with no warn; for
None / null / non-string fall through to InheritancePolicy::OverrideOnly
silently; and only call tracing::warn! when Some(v).as_str() exists but is not
equal to either known token (i.e., truly unknown string).

---

Outside diff comments:
In
`@modules/system/account-management/account-management/src/domain/conversion/repo.rs`:
- Around line 85-101: Update the scope contract comment for the
conversion_requests entity to reflect its current declaration: replace the
incorrect `Scopable(no_tenant, no_resource, no_owner, no_type)` wording with an
entry that shows the tenant column (e.g., `tenant_col = "tenant_id"`) so the
comment matches the actual entity declaration; locate the comment block
referencing `Scopable` and `conversion_requests` and change `no_tenant` to
`tenant_col = "tenant_id"` while keeping the rest of the explanation about scope
behavior and callers using `AccessScope::allow_all` intact.

In
`@modules/system/account-management/account-management/src/domain/conversion/service.rs`:
- Around line 1458-1464: Add the same nil-actor guard to the start of the
approve method: check if approver_uuid == Uuid::nil() and return the same
DomainError used by request_conversion/cancel/reject (i.e., the
invalid/empty-actor error) instead of proceeding; mirror the exact guard pattern
and error return used in request_conversion, cancel and reject so approve (in
the approve(&self, scope: &AccessScope, request_id: Uuid, caller:
ConversionCaller, approver_uuid: Uuid) -> ...) enforces the same nil-UUID
validation and preserves audit consistency.

---

Nitpick comments:
In
`@modules/system/account-management/account-management/src/domain/metadata/service.rs`:
- Around line 443-478: The delete_for_tenant service forwards the repo error
verbatim which embeds the internal schema_uuid in
DomainError::MetadataEntryNotFound; change the call to
metadata_repo.delete_for_tenant(...) in delete_for_tenant to map_err and, if the
error is DomainError::MetadataEntryNotFound, rebuild/convert it so the
entry/detail use the public schema_id (the existing schema_id variable) instead
of schema_uuid, otherwise propagate the original error; keep the rest of the
function (resolve_active_tenant, resolve_inheritance_policy, derive_schema_uuid,
logging) unchanged.

In
`@modules/system/account-management/account-management/src/domain/user_groups/cascade.rs`:
- Around line 322-351: The code currently returns retryable errors inside the
CASCADE_TIMEOUT blocks for client.list_memberships and client.remove_membership
without emitting AM_DEPENDENCY_HEALTH counters; update the error paths in
cascade.rs around the match handling of tokio::time::timeout for
client.list_memberships (the Err(_) timeout branch and the Ok(Err(e)) branch)
and for client.remove_membership (the Err(_) timeout branch and the Ok(Err(e))
non-NotFound branch) to increment the AM_DEPENDENCY_HEALTH metric before
returning retryable, using a consistent label set (e.g.,
dependency="resource_group", operation="list_memberships" or
"remove_membership", status="timeout" or status="error") so failures are
attributed the same way as other cascade paths.

In
`@modules/system/account-management/account-management/tests/metadata_integration_pg.rs`:
- Around line 193-204: The test builds raw DELETE SQL via format! into the
variable raw and executes it with h.ddl_conn.execute_unprepared which bypasses
parameterized queries; update the calls to use the DDL connection's
parameterized execution API (instead of building the string) so the DELETE
statements for tenant_closure and tenants pass the target UUID as a bind
parameter, or if the DDL test client truly lacks parameter support, add a short
comment above these lines referencing execute_unprepared and explaining why
string interpolation is safe here (test-only and UUID-only) and link to the
test-only justification.
🪄 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: ce739831-002b-44ba-aa63-cf4b9d176315

📥 Commits

Reviewing files that changed from the base of the PR and between 5f81b3d and 604f324.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (41)
  • modules/system/account-management/account-management-sdk/Cargo.toml
  • modules/system/account-management/account-management-sdk/src/lib.rs
  • modules/system/account-management/account-management-sdk/src/metadata.rs
  • modules/system/account-management/account-management-sdk/src/metadata_tests.rs
  • modules/system/account-management/account-management/src/domain/conversion/repo.rs
  • modules/system/account-management/account-management/src/domain/conversion/service.rs
  • modules/system/account-management/account-management/src/domain/metadata/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/registry.rs
  • modules/system/account-management/account-management/src/domain/metadata/repo.rs
  • modules/system/account-management/account-management/src/domain/metadata/service.rs
  • modules/system/account-management/account-management/src/domain/metadata/service_tests.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rs
  • modules/system/account-management/account-management/src/domain/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/repo.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/auth.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs
  • modules/system/account-management/account-management/src/domain/user_groups/mod.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration_tests.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/tenants.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/metadata.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • modules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs
  • modules/system/account-management/account-management/src/infra/types_registry/mod.rs
  • modules/system/account-management/account-management/src/lib.rs
  • modules/system/account-management/account-management/src/module.rs
  • modules/system/account-management/account-management/tests/metadata_integration.rs
  • modules/system/account-management/account-management/tests/metadata_integration_pg.rs

@diffora
diffora force-pushed the am/05-tenant-metadata branch 4 times, most recently from 5512386 to 1befb4a Compare May 12, 2026 11:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
modules/system/account-management/account-management/src/domain/conversion/service.rs (2)

1683-1685: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Convert debug-only assertion to runtime guard.

Same issue as in soft_delete_resolved: the debug_assert! should be a runtime guard that returns an error in all builds.

🤖 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
`@modules/system/account-management/account-management/src/domain/conversion/service.rs`
around lines 1683 - 1685, The debug-only assertion in expire_pending that checks
scope.is_unconstrained() must be converted into a runtime guard that returns an
appropriate error instead of only panicking in debug builds; replace the
debug_assert! with an if !scope.is_unconstrained() { return Err(...) } check
(mirroring the behavior used in soft_delete_resolved) and return the same error
type/value your function uses for invalid scope checks, referencing
expire_pending, soft_delete_resolved, and AccessScope::allow_all to locate and
match the existing runtime-guard pattern.

1409-1412: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Convert debug-only assertion to runtime guard.

The debug_assert! means production builds silently ignore a narrowed scope. This should fail closed with a runtime error in all builds.

🤖 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
`@modules/system/account-management/account-management/src/domain/conversion/service.rs`
around lines 1409 - 1412, The debug-only assertion in soft_delete_resolved uses
debug_assert!(scope.is_unconstrained(), ...) which is ignored in release;
replace it with a runtime guard: check if !scope.is_unconstrained() and return a
meaningful error (or Err variant) from soft_delete_resolved carrying the same
message (or use anyhow::bail!/return Err(…) depending on the function's error
type) so releases fail closed instead of silently proceeding.
🧹 Nitpick comments (1)
modules/system/account-management/account-management-sdk/Cargo.toml (1)

46-53: 💤 Low value

Move test-only macros feature to [dev-dependencies].

The inline comment notes macros is requested so unit tests can use time::macros::datetime!. Since cargo features unify per-target rather than per-dependency-table, enabling macros in [dependencies] propagates it to every downstream consumer of cyberware-account-management-sdk, not just this crate's tests. Keep serde-well-known in [dependencies] (it's part of the public wire contract), and move macros into [dev-dependencies] so the SDK's prod surface stays minimal.

♻️ Proposed split
-# `time` carries `MetadataEntry::updated_at` in the metadata SDK
-# module. The workspace pin already enables `serde` / `formatting` /
-# `parsing`; we additionally request `serde-well-known` so the field
-# wire-serialises as RFC 3339 (the default `time` serde format is a
-# tuple, which would leak into every public metadata response body).
-# `macros` is requested so unit tests can spell timestamps with
-# `time::macros::datetime!`.
-time = { workspace = true, features = ["serde-well-known", "macros"] }
+# `time` carries `MetadataEntry::updated_at` in the metadata SDK
+# module. The workspace pin already enables `serde` / `formatting` /
+# `parsing`; we additionally request `serde-well-known` so the field
+# wire-serialises as RFC 3339 (the default `time` serde format is a
+# tuple, which would leak into every public metadata response body).
+time = { workspace = true, features = ["serde-well-known"] }

 [dev-dependencies]
 tokio = { workspace = true, features = ["macros", "rt"] }
+# `macros` enables `time::macros::datetime!` in unit tests; kept out
+# of `[dependencies]` so the feature does not propagate to consumers.
+time = { workspace = true, features = ["macros"] }
🤖 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 `@modules/system/account-management/account-management-sdk/Cargo.toml` around
lines 46 - 53, The Cargo.toml currently enables the `macros` feature of the
`time` dependency under regular `[dependencies]`, which leaks a test-only
feature to consumers; change this by keeping `time = { workspace = true,
features = ["serde-well-known"] }` in the normal dependency list and moving the
`macros` feature into `[dev-dependencies]` (e.g. add `time = { workspace = true,
features = ["macros"] }` under `[dev-dependencies]`) so `serde-well-known`
remains for the public wire contract while `time::macros::datetime!` support is
only available for tests.
🤖 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
`@modules/system/account-management/account-management-sdk/src/metadata_tests.rs`:
- Around line 233-264: The test
metadata_validation_error_display_strings_are_pinned omitted the
MetadataValidationError::NotASchemaId variant; add an assert_eq! that constructs
MetadataValidationError::NotASchemaId { actual: "<example>".into() } and
compares .to_string() to the exact pinned message the Display implementation
produces (including the interpolated actual value) so the NotASchemaId arm is
locked the same way as
MalformedSchemaId/WrongRootSegment/MissingChainedSegment/EmptyValue.

In
`@modules/system/account-management/account-management/src/domain/user/service.rs`:
- Around line 818-825: The doc comment in account-management::user::service.rs
incorrectly states transport errors surface as `DomainError::IdpUnavailable`;
update the narrative to match the implementation and the `# Errors` section by
stating transport/transport-layer failures surface as
`DomainError::ServiceUnavailable` (i.e., via
`DomainError::service_unavailable(...)`), or remove the misleading
`IdpUnavailable` mention and clarify that `IdpUnavailable` is reserved for the
IdP plugin call site; keep references to `DomainError::service_unavailable` and
the IdP plugin so readers can find the relevant code paths (e.g., the error
returns in the functions that call `DomainError::service_unavailable(...)`).

---

Duplicate comments:
In
`@modules/system/account-management/account-management/src/domain/conversion/service.rs`:
- Around line 1683-1685: The debug-only assertion in expire_pending that checks
scope.is_unconstrained() must be converted into a runtime guard that returns an
appropriate error instead of only panicking in debug builds; replace the
debug_assert! with an if !scope.is_unconstrained() { return Err(...) } check
(mirroring the behavior used in soft_delete_resolved) and return the same error
type/value your function uses for invalid scope checks, referencing
expire_pending, soft_delete_resolved, and AccessScope::allow_all to locate and
match the existing runtime-guard pattern.
- Around line 1409-1412: The debug-only assertion in soft_delete_resolved uses
debug_assert!(scope.is_unconstrained(), ...) which is ignored in release;
replace it with a runtime guard: check if !scope.is_unconstrained() and return a
meaningful error (or Err variant) from soft_delete_resolved carrying the same
message (or use anyhow::bail!/return Err(…) depending on the function's error
type) so releases fail closed instead of silently proceeding.

---

Nitpick comments:
In `@modules/system/account-management/account-management-sdk/Cargo.toml`:
- Around line 46-53: The Cargo.toml currently enables the `macros` feature of
the `time` dependency under regular `[dependencies]`, which leaks a test-only
feature to consumers; change this by keeping `time = { workspace = true,
features = ["serde-well-known"] }` in the normal dependency list and moving the
`macros` feature into `[dev-dependencies]` (e.g. add `time = { workspace = true,
features = ["macros"] }` under `[dev-dependencies]`) so `serde-well-known`
remains for the public wire contract while `time::macros::datetime!` support is
only available for tests.
🪄 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: 6c9cd0b7-6e74-455b-977d-9c55610fb2ce

📥 Commits

Reviewing files that changed from the base of the PR and between 604f324 and 1befb4a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (44)
  • modules/system/account-management/account-management-sdk/Cargo.toml
  • modules/system/account-management/account-management-sdk/src/gts.rs
  • modules/system/account-management/account-management-sdk/src/lib.rs
  • modules/system/account-management/account-management-sdk/src/metadata.rs
  • modules/system/account-management/account-management-sdk/src/metadata_tests.rs
  • modules/system/account-management/account-management/src/domain/conversion/repo.rs
  • modules/system/account-management/account-management/src/domain/conversion/service.rs
  • modules/system/account-management/account-management/src/domain/metadata/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/registry.rs
  • modules/system/account-management/account-management/src/domain/metadata/repo.rs
  • modules/system/account-management/account-management/src/domain/metadata/service.rs
  • modules/system/account-management/account-management/src/domain/metadata/service_tests.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rs
  • modules/system/account-management/account-management/src/domain/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/repo.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/auth.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/user/service.rs
  • modules/system/account-management/account-management/src/domain/user/service_tests.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs
  • modules/system/account-management/account-management/src/domain/user_groups/mod.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration_tests.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/tenants.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/metadata.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • modules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs
  • modules/system/account-management/account-management/src/infra/types_registry/mod.rs
  • modules/system/account-management/account-management/src/lib.rs
  • modules/system/account-management/account-management/src/module.rs
  • modules/system/account-management/account-management/tests/metadata_integration.rs
  • modules/system/account-management/account-management/tests/metadata_integration_pg.rs
✅ Files skipped from review due to trivial changes (6)
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • modules/system/account-management/account-management/src/domain/conversion/repo.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rs
  • modules/system/account-management/account-management/src/domain/tenant/repo.rs
🚧 Files skipped from review as they are similar to previous changes (23)
  • modules/system/account-management/account-management/src/domain/metadata/test_support/mod.rs
  • modules/system/account-management/account-management-sdk/src/lib.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rs
  • modules/system/account-management/account-management/src/domain/user_groups/mod.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade.rs
  • modules/system/account-management/account-management/src/domain/metadata/registry.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/auth.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/tenants.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs
  • modules/system/account-management/account-management/src/domain/metadata/repo.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • modules/system/account-management/account-management/tests/metadata_integration_pg.rs
  • modules/system/account-management/account-management/tests/metadata_integration.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • modules/system/account-management/account-management/src/module.rs
  • modules/system/account-management/account-management/src/domain/metadata/service.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rs
  • modules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs

Comment thread modules/system/account-management/account-management/src/domain/user/service.rs Outdated
@diffora
diffora force-pushed the am/05-tenant-metadata branch 2 times, most recently from 5bcb622 to fc1466c Compare May 12, 2026 14:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/system/account-management/account-management/src/domain/user/service.rs (1)

421-431: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update deprovision_user docs to include cleanup-originated errors.

Line 421’s # Errors list is now incomplete: deprovision_user can return DomainError::ServiceUnavailable (RG cleanup timeout/transport failure) and DomainError::Internal (e.g., invalid RG cursor decode at Line 1091).

📝 Proposed doc patch
 /// # Errors
 ///
 /// * [`DomainError::NotFound`] -- `tenant_id` does not resolve.
 /// * [`DomainError::Validation`] -- tenant exists but is not
 ///   [`TenantStatus::Active`].
 /// * [`DomainError::IdpUnavailable`] -- transport failure or
 ///   timeout on the `IdP` call.
 /// * [`DomainError::UnsupportedOperation`] -- provider declined
 ///   the operation.
 /// * [`DomainError::Validation`] -- provider rejected the request.
+/// * [`DomainError::ServiceUnavailable`] -- post-IdP RG membership
+///   cleanup failed (transport/per-call timeout/overall budget).
+/// * [`DomainError::Internal`] -- unexpected RG cursor/error shape
+///   during cleanup.
🤖 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
`@modules/system/account-management/account-management/src/domain/user/service.rs`
around lines 421 - 431, Update the deprovision_user documentation error list to
include cleanup-originated errors: add DomainError::ServiceUnavailable to cover
RG cleanup timeouts or transport failures and DomainError::Internal to cover
internal failures such as invalid RG cursor decode encountered during cleanup
(see deprovision_user and the RG cursor handling around the RG cleanup/cursor
decode logic). Ensure both variants are documented alongside the existing
NotFound, Validation, IdpUnavailable, UnsupportedOperation entries so callers
are aware cleanup can produce ServiceUnavailable or Internal errors.
🧹 Nitpick comments (3)
modules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs (1)

43-44: ⚡ Quick win

Make the fake validate ODataQuery to catch filter regressions.

Line 64–70 currently ignores _query, so tests won’t fail if fetch_tenant_groups builds the wrong filter (tenant/type/root).

Suggested patch
-type ListGroupsFn = Box<dyn Fn() -> Result<Page<ResourceGroup>, ResourceGroupError> + Send + Sync>;
+type ListGroupsFn =
+    Box<dyn Fn(&ODataQuery) -> Result<Page<ResourceGroup>, ResourceGroupError> + Send + Sync>;

 async fn list_groups(
     &self,
     _ctx: &SecurityContext,
-    _query: &ODataQuery,
+    query: &ODataQuery,
 ) -> Result<Page<ResourceGroup>, ResourceGroupError> {
-    (self.list_groups_fn)()
+    (self.list_groups_fn)(query)
 }

Also applies to: 64-70

🤖 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
`@modules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs`
around lines 43 - 44, The fake ListGroupsFn used in cascade_tests.rs currently
ignores its ODataQuery argument allowing filter regressions to slip through;
update the mock (the closure implementing ListGroupsFn) to inspect the incoming
ODataQuery/filter produced by fetch_tenant_groups and assert (or return
Err(ResourceGroupError::InvalidQuery)) if the filter does not include the
expected tenant, type, and root predicates, so tests fail when the query is
built incorrectly. Ensure the check is applied where the boxed Fn is created
(the test-local ListGroupsFn closure) and keep DeleteCascadeFn behavior
unchanged.
modules/system/account-management/account-management/src/domain/user_groups/cascade.rs (1)

328-333: ⚡ Quick win

Emit dependency-health metric on cursor decode failures.

Line 331–333 returns Retryable but skips metric emission, unlike other cascade_list_groups failure branches.

Suggested patch
         match page.page_info.next_cursor {
             Some(token) => {
-                cursor = Some(
-                    CursorV1::decode(&token)
-                        .map_err(|e| retryable(format!("invalid cursor from RG: {e}")))?,
-                );
+                cursor = Some(match CursorV1::decode(&token) {
+                    Ok(c) => c,
+                    Err(e) => {
+                        emit_metric(
+                            AM_DEPENDENCY_HEALTH,
+                            MetricKind::Counter,
+                            &[
+                                ("target", "resource_group"),
+                                ("op", "cascade_list_groups"),
+                                ("outcome", "error"),
+                            ],
+                        );
+                        return Err(retryable(format!("invalid cursor from RG: {e}")));
+                    }
+                });
             }
             None => break,
         }
🤖 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
`@modules/system/account-management/account-management/src/domain/user_groups/cascade.rs`
around lines 328 - 333, The CursorV1::decode failure path inside
cascade_list_groups currently returns a Retryable but doesn't emit the
dependency-health metric like the other failure branches; update the map_err
handling around CursorV1::decode to emit the same dependency-health metric used
by the other cascade_list_groups error branches (i.e., call the existing metric
helper used elsewhere in cascade_list_groups for RG/dependency failures) before
returning the retryable error so decode errors are recorded the same way as
other downstream failures.
modules/system/account-management/account-management/src/domain/user/service_tests.rs (1)

785-789: ⚡ Quick win

Assert the outbound list_groups query shape in the fake.

FakeMembershipRgClient::list_groups ignores Line 788’s _query, so these tests won’t catch regressions that accidentally drop tenant/type filtering in list_tenant_user_groups. Please capture/assert query semantics (tenant_id + USER_GROUP_RG_TYPE_CODE) in at least one cleanup test.

🤖 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
`@modules/system/account-management/account-management/src/domain/user/service_tests.rs`
around lines 785 - 789, The fake implementation
FakeMembershipRgClient::list_groups currently ignores the _query parameter, so
update the fake to capture and assert the ODataQuery passed by
list_tenant_user_groups: have FakeMembershipRgClient::list_groups accept the
query, validate (or record) that it contains the expected tenant_id and the
USER_GROUP_RG_TYPE_CODE filter, and fail the test if not present; then add an
assertion in at least one cleanup test that inspects the recorded query or
relies on the fake's validation to ensure tenant/type filtering is applied by
list_tenant_user_groups.
🤖 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.

Outside diff comments:
In
`@modules/system/account-management/account-management/src/domain/user/service.rs`:
- Around line 421-431: Update the deprovision_user documentation error list to
include cleanup-originated errors: add DomainError::ServiceUnavailable to cover
RG cleanup timeouts or transport failures and DomainError::Internal to cover
internal failures such as invalid RG cursor decode encountered during cleanup
(see deprovision_user and the RG cursor handling around the RG cleanup/cursor
decode logic). Ensure both variants are documented alongside the existing
NotFound, Validation, IdpUnavailable, UnsupportedOperation entries so callers
are aware cleanup can produce ServiceUnavailable or Internal errors.

---

Nitpick comments:
In
`@modules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs`:
- Around line 43-44: The fake ListGroupsFn used in cascade_tests.rs currently
ignores its ODataQuery argument allowing filter regressions to slip through;
update the mock (the closure implementing ListGroupsFn) to inspect the incoming
ODataQuery/filter produced by fetch_tenant_groups and assert (or return
Err(ResourceGroupError::InvalidQuery)) if the filter does not include the
expected tenant, type, and root predicates, so tests fail when the query is
built incorrectly. Ensure the check is applied where the boxed Fn is created
(the test-local ListGroupsFn closure) and keep DeleteCascadeFn behavior
unchanged.

In
`@modules/system/account-management/account-management/src/domain/user_groups/cascade.rs`:
- Around line 328-333: The CursorV1::decode failure path inside
cascade_list_groups currently returns a Retryable but doesn't emit the
dependency-health metric like the other failure branches; update the map_err
handling around CursorV1::decode to emit the same dependency-health metric used
by the other cascade_list_groups error branches (i.e., call the existing metric
helper used elsewhere in cascade_list_groups for RG/dependency failures) before
returning the retryable error so decode errors are recorded the same way as
other downstream failures.

In
`@modules/system/account-management/account-management/src/domain/user/service_tests.rs`:
- Around line 785-789: The fake implementation
FakeMembershipRgClient::list_groups currently ignores the _query parameter, so
update the fake to capture and assert the ODataQuery passed by
list_tenant_user_groups: have FakeMembershipRgClient::list_groups accept the
query, validate (or record) that it contains the expected tenant_id and the
USER_GROUP_RG_TYPE_CODE filter, and fail the test if not present; then add an
assertion in at least one cleanup test that inspects the recorded query or
relies on the fake's validation to ensure tenant/type filtering is applied by
list_tenant_user_groups.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aca07db1-4017-44d6-af85-7d09dda65cf2

📥 Commits

Reviewing files that changed from the base of the PR and between 1befb4a and 6f5f120.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (53)
  • modules/system/account-management/account-management-sdk/Cargo.toml
  • modules/system/account-management/account-management-sdk/src/gts.rs
  • modules/system/account-management/account-management-sdk/src/lib.rs
  • modules/system/account-management/account-management-sdk/src/metadata.rs
  • modules/system/account-management/account-management-sdk/src/metadata_tests.rs
  • modules/system/account-management/account-management/Cargo.toml
  • modules/system/account-management/account-management/src/domain/conversion/repo.rs
  • modules/system/account-management/account-management/src/domain/conversion/service.rs
  • modules/system/account-management/account-management/src/domain/metadata/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/registry.rs
  • modules/system/account-management/account-management/src/domain/metadata/repo.rs
  • modules/system/account-management/account-management/src/domain/metadata/service.rs
  • modules/system/account-management/account-management/src/domain/metadata/service_tests.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rs
  • modules/system/account-management/account-management/src/domain/metrics.rs
  • modules/system/account-management/account-management/src/domain/mod.rs
  • modules/system/account-management/account-management/src/domain/ports/metrics.rs
  • modules/system/account-management/account-management/src/domain/ports/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/repo.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/auth.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/user/service.rs
  • modules/system/account-management/account-management/src/domain/user/service_tests.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade.rs
  • modules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs
  • modules/system/account-management/account-management/src/domain/user_groups/mod.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration_tests.rs
  • modules/system/account-management/account-management/src/infra/metrics.rs
  • modules/system/account-management/account-management/src/infra/metrics_tests.rs
  • modules/system/account-management/account-management/src/infra/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/tenants.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/metadata.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • modules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs
  • modules/system/account-management/account-management/src/infra/types_registry/mod.rs
  • modules/system/account-management/account-management/src/lib.rs
  • modules/system/account-management/account-management/src/module.rs
  • modules/system/account-management/account-management/tests/metadata_integration.rs
  • modules/system/account-management/account-management/tests/metadata_integration_pg.rs
  • modules/system/resource-group/resource-group-sdk/src/api.rs
  • modules/system/resource-group/resource-group/src/domain/rg_service.rs
✅ Files skipped from review due to trivial changes (8)
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • modules/system/account-management/account-management/src/domain/tenant/repo.rs
  • modules/system/account-management/account-management/src/lib.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rs
  • modules/system/account-management/account-management/src/domain/conversion/repo.rs
🚧 Files skipped from review as they are similar to previous changes (30)
  • modules/system/account-management/account-management-sdk/src/lib.rs
  • modules/system/account-management/account-management/src/infra/types_registry/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/mod.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/tenants.rs
  • modules/system/account-management/account-management/src/domain/conversion/service.rs
  • modules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rs
  • modules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rs
  • modules/system/account-management/account-management-sdk/src/gts.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rs
  • modules/system/account-management/account-management/src/domain/metadata/repo.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/auth.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration.rs
  • modules/system/account-management/account-management/tests/metadata_integration_pg.rs
  • modules/system/account-management/account-management/src/domain/metadata/mod.rs
  • modules/system/account-management/account-management/src/domain/metadata/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • modules/system/account-management/account-management/src/domain/user_groups/mod.rs
  • modules/system/account-management/account-management/src/module.rs
  • modules/system/account-management/account-management-sdk/Cargo.toml
  • modules/system/account-management/account-management-sdk/src/metadata_tests.rs
  • modules/system/account-management/account-management/tests/metadata_integration.rs
  • modules/system/account-management/account-management/src/domain/user_groups/registration_tests.rs
  • modules/system/account-management/account-management/src/domain/mod.rs
  • modules/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • modules/system/account-management/account-management/src/domain/metadata/registry.rs
  • modules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs
  • modules/system/account-management/account-management/src/domain/metadata/service.rs
  • modules/system/account-management/account-management/src/infra/storage/repo_impl/metadata.rs

@diffora
diffora force-pushed the am/05-tenant-metadata branch 5 times, most recently from 8ba51a3 to 852005b Compare May 12, 2026 17:28
@diffora diffora changed the title feat(account-management): tenant-metadata + AM tenant subtree clamp feat(account-management): tenant-metadata + AM subtree clamp + observability ports May 12, 2026
@diffora
diffora force-pushed the am/05-tenant-metadata branch 2 times, most recently from 8db3e4c to e10f5b9 Compare May 12, 2026 18:58
…ability ports

End-to-end tenant-metadata feature, AM-side InTenantSubtree clamp on
tenant CRUD, and typed OpenTelemetry observability ports for the AM
metric catalog.

## Tenant-metadata

* SDK contracts (account-management-sdk/metadata.rs + tests).
* Domain service + repo with own-first / barrier-stop /
  suspended-skip walk-up; GTS-backed metadata schema registry;
  migration m0005; PG integration tests.
* User-groups cascade hook fires on tenant hard-delete before the
  IdP call.

## InTenantSubtree clamp (cyberware-rust#1813)

* `tenants` entity declares `resource_col = "id"`, so the compiled
  AccessScope materialises a tenant_closure subtree-clamp JOIN at
  the database layer rather than only at the service-level PDP
  gate.
* TenantService::authorize switches to `require_constraints(true)`
  with both OWNER_TENANT_ID and RESOURCE_ID set, so a missing PDP
  enforce-point fails closed via CompileFailed ->
  CrossTenantDenied (HTTP 403) instead of widening silently to
  allow_all.
* read_tenant / list_children / update_tenant / soft_delete
  forward the compiled scope into the repo; positive +
  cross-subtree negative tests pin both directions.
* conversion_requests entity declares `tenant_col = "tenant_id"`
  as forward-compat for the REST PR; service still passes
  allow_all to the conversion repo because dual-consent flows
  need barrier penetration on self-managed children.
* MetadataService walk-up uses allow_all for ancestor reads --
  the start tenant has been PEP-authorized; ancestors are outside
  the caller's subtree by definition of the new tenants clamp.
* AccountManagementModule advertises Capability::TenantHierarchy
  so the PDP returns the native InTenantSubtree predicate rather
  than pre-resolved In.

## Observability ports + OTel adapter

* New domain/ports/metrics.rs with seven segregated trait
  families (Bootstrap, Conversion, Dependency, Integrity,
  Metadata, Tenant, Storage) and a typed label-value taxonomy.
  Sealed-newtype bridges (BootstrapClassification,
  DependencyOutcome, TenantRetentionOutcome) accept existing SDK
  / domain failure enums via From<&Failure>, so as_metric_label()
  owns the variant->string mapping uniquely. Every type carries
  the #[domain_model] attribute required by DE0309.
* New infra/metrics.rs holding AmMetricsMeter: 12 counters, 4
  gauges, 1 histogram, all pre-built against
  global::meter_with_scope("account-management"). Implements
  every port trait and the transitional MetricsFacadeBridge that
  routes legacy emit_metric / emit_gauge_value /
  emit_histogram_value calls by family constant.
* domain/metrics.rs: bridge trait + LazyLock<ArcSwap<Option<...>>>
  for lock-free reads and replace-on-reinit semantics;
  install_facade_bridge returns true on first install / false on
  replace.
* module.rs: install_facade_bridge(build_default_adapter()) only
  when modkit::telemetry::metrics_provider_installed() is true,
  so the NoopMeterProvider posture (opentelemetry.metrics
  disabled) keeps the emit_* helpers on their silent no-op
  fast path with zero KeyValue allocation. Tagged info logs
  (`kind = metrics_init_skipped` / `metrics_bridge_replaced`)
  for operator filterability.
* libs/modkit/src/telemetry: new metrics_provider_installed()
  reader and mark_metrics_provider_installed() escape hatch for
  embedders that wire OTel outside modkit bootstrap
  (api-gateway/tests/http_metrics_tests.rs uses it after a direct
  global::set_meter_provider).
* InMemoryMetricExporter end-to-end tests in
  infra/metrics_tests.rs cover typed port methods (counters,
  gauges, histograms), optional labels, threshold rendering,
  From<&Failure> bridges, and all three facade-bridge kinds.
  Domain-side port unit tests live in
  domain/ports/metrics_tests.rs per the DE1101 sibling-file
  convention.

Metric names keep the dot-separated form (am.dependency_health
etc.) -- unit hints attach via .with_unit("ms") /
.with_unit("s") on the adapter rather than embedded in the
metric name.

The bridge and the emit_* facade are transitional: per-family
call-site migration onto the typed ports proceeds in follow-up
PRs; the bridge is removed once every call site has moved.

Signed-off-by: Diffora <ddiffora@gmail.com>
@diffora
diffora force-pushed the am/05-tenant-metadata branch from e10f5b9 to 9c797be Compare May 12, 2026 19:31
diffora added a commit that referenced this pull request May 13, 2026
* #15: partial-TX rollback on the apply seam's TOCTOU type guard.
  After ConversionService::approve runs its pre-apply
  TenantTypeChecker check, the converting tenant's
  tenant_type_uuid is mutated out of band. The apply TX MUST
  reject with Validation (expected_tenant_type_uuid mismatch) and
  leave every other piece of state intact: pending row stays
  Pending, tenants.self_managed unchanged, closure barriers
  unchanged. SQLite integration test.

* #8 (partial): cross-tenant denial coverage for the conversion
  service's request_conversion and approve seams, plus the user
  service's provision_user. cancel + reject already had the
  fence; this commit closes the asymmetry on the remaining seams
  flagged by aviator5. Broader fan-out to list_own_for_tenant /
  list_inbound_for_parent and the user-side deprovision_user /
  list_users left as a follow-up issue — the load-bearing fence
  (require_caller_scope_or_not_found + scope-clamped tenant_repo
  lookup) is identical across these methods, so the pattern is
  pinned by the four tests landed here.

* #20: provision_user happy path with the
  gts.cf.core.am.user.v1~ schema actually registered. The
  existing test silently exercised the schema-not-found
  short-circuit; the new test pins that the service-layer
  validator traverses the registered-schema path without
  short-circuiting.

* #22: DoS surface tests on validate_new_user_payload_via_gts —
  1 MB string value and 256-level nested object on the
  free-form `attributes` field. The current contract accepts
  both; the tests pin "no panic / no stack overflow / no hang"
  as the boundary. A future PR that adds an AM-side cap (max
  attributes byte size or recursion depth) flips the assertions
  to Validation without breaking the tests.

Follow-up (not in this PR):
  - #14: module.rs::serve() ordering unit tests (4 fail-closed
    paths). Each path is currently exercised end-to-end through
    the integration harness; isolated unit tests on serve()
    require a sizeable mock harness around ModuleCtx that does
    not exist today.
  - #16: PG mirror of expire_tick + retention_soft_delete from
    SQLite integration. The SQLite suite covers the data-shape
    contract; the PG-only contract pins SERIALIZABLE behaviour
    that the existing pg_concurrent_approves_serialize_to_one_winner
    test already covers for the approve seam.
  - #8 broader: list_own / list_inbound / deprovision / list_users
    cross-tenant denial fan-out.

All 456 unit tests + 11 SQLite integration tests + clippy-clean.
diffora added a commit that referenced this pull request May 13, 2026
Phase 1 dual-consent conversion lifecycle + IdP user-operations
contract. Domain + storage land here; REST handlers in a follow-up.

* Conversion state machine (pending → approved/cancelled/rejected/
  expired) with partial-unique-pending invariant, dual-consent
  apply seam, type-stability TOCTOU guards, reaper + retention
  sweeps with `ConversionScope` + `CancellationToken`.
* IdP user ops: `list_users` cursor pagination (MAX_TOP=200),
  `deprovision_user → Result<()>`, `IdpPluginSpecV1`.
* IdP-metadata opaque-proxy contract: `tenant_idp_metadata` table,
  `TenantContext.metadata: Option<Value>`, mandatory `tenant_type`.
* m0004 migration creates `conversion_requests` + `tenant_idp_metadata`
  with per-backend DDL; SQLite cascade backed by explicit DELETE
  in `hard_delete_one`.
* Deep-review aviator5#4434462671: 20/22 findings closed; #4/#10/#14/#16
  deferred to InTenantSubtree adoption (cyberfabric-core#1813).

Signed-off-by: Diffora <ddiffora@gmail.com>
@diffora
diffora force-pushed the am/04-conversion-service branch from eb331e4 to 4892b62 Compare May 13, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant