feat(account-management): tenant-metadata + AM subtree clamp + observability ports#16
feat(account-management): tenant-metadata + AM subtree clamp + observability ports#16diffora wants to merge 2 commits into
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winAdd the same nil-actor guard in
approveas other transition methods.
request_conversion,cancel, andrejectrejectUuid::nil()actor IDs, butapprovedoes 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 winUpdate scope contract doc to match the actual
conversion_requestsentity declaration.The doc at lines 85–86 states
Scopable(no_tenant, no_resource, no_owner, no_type), but the entity declarestenant_col = "tenant_id". Replaceno_tenantwithtenant_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 valueConsider 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 winEmit dependency-health metrics for membership list/remove failures
Line 322-Line 351 returns retryable on RG failures/timeouts but skips
AM_DEPENDENCY_HEALTHemission, 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_tenantsurfacesMetadataEntryNotFoundwith the internalschema_uuid, breaking error-shape parity withget_for_tenant.The repo's
delete_for_tenantbuilds the error fromschema_uuid(lines 312-315 ofinfra/storage/repo_impl/metadata.rs), while the service'sget_for_tenantbuilds the sameDomainError::MetadataEntryNotFoundfrom the public chainedschema_id(lines 292-295 here). Forwarding the repo error verbatim means clients hitting the distinct-404 contract see two differententry/detailshapes 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 onentrywill have to special-case.Since the public
schema_idis already in scope at the call site, a small.map_errkeeps 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (41)
modules/system/account-management/account-management-sdk/Cargo.tomlmodules/system/account-management/account-management-sdk/src/lib.rsmodules/system/account-management/account-management-sdk/src/metadata.rsmodules/system/account-management/account-management-sdk/src/metadata_tests.rsmodules/system/account-management/account-management/src/domain/conversion/repo.rsmodules/system/account-management/account-management/src/domain/conversion/service.rsmodules/system/account-management/account-management/src/domain/metadata/mod.rsmodules/system/account-management/account-management/src/domain/metadata/registry.rsmodules/system/account-management/account-management/src/domain/metadata/repo.rsmodules/system/account-management/account-management/src/domain/metadata/service.rsmodules/system/account-management/account-management/src/domain/metadata/service_tests.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/mod.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/repo.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rsmodules/system/account-management/account-management/src/domain/mod.rsmodules/system/account-management/account-management/src/domain/tenant/repo.rsmodules/system/account-management/account-management/src/domain/tenant/service/mod.rsmodules/system/account-management/account-management/src/domain/tenant/service/service_tests.rsmodules/system/account-management/account-management/src/domain/tenant/test_support/auth.rsmodules/system/account-management/account-management/src/domain/tenant/test_support/repo.rsmodules/system/account-management/account-management/src/domain/user_groups/cascade.rsmodules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rsmodules/system/account-management/account-management/src/domain/user_groups/mod.rsmodules/system/account-management/account-management/src/domain/user_groups/registration.rsmodules/system/account-management/account-management/src/domain/user_groups/registration_tests.rsmodules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rsmodules/system/account-management/account-management/src/infra/storage/entity/tenants.rsmodules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rsmodules/system/account-management/account-management/src/infra/storage/migrations/mod.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/metadata.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rsmodules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rsmodules/system/account-management/account-management/src/infra/types_registry/mod.rsmodules/system/account-management/account-management/src/lib.rsmodules/system/account-management/account-management/src/module.rsmodules/system/account-management/account-management/tests/metadata_integration.rsmodules/system/account-management/account-management/tests/metadata_integration_pg.rs
5512386 to
1befb4a
Compare
There was a problem hiding this comment.
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 winConvert debug-only assertion to runtime guard.
Same issue as in
soft_delete_resolved: thedebug_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 winConvert 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 valueMove test-only
macrosfeature to[dev-dependencies].The inline comment notes
macrosis requested so unit tests can usetime::macros::datetime!. Since cargo features unify per-target rather than per-dependency-table, enablingmacrosin[dependencies]propagates it to every downstream consumer ofcyberware-account-management-sdk, not just this crate's tests. Keepserde-well-knownin[dependencies](it's part of the public wire contract), and movemacrosinto[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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (44)
modules/system/account-management/account-management-sdk/Cargo.tomlmodules/system/account-management/account-management-sdk/src/gts.rsmodules/system/account-management/account-management-sdk/src/lib.rsmodules/system/account-management/account-management-sdk/src/metadata.rsmodules/system/account-management/account-management-sdk/src/metadata_tests.rsmodules/system/account-management/account-management/src/domain/conversion/repo.rsmodules/system/account-management/account-management/src/domain/conversion/service.rsmodules/system/account-management/account-management/src/domain/metadata/mod.rsmodules/system/account-management/account-management/src/domain/metadata/registry.rsmodules/system/account-management/account-management/src/domain/metadata/repo.rsmodules/system/account-management/account-management/src/domain/metadata/service.rsmodules/system/account-management/account-management/src/domain/metadata/service_tests.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/mod.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/repo.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rsmodules/system/account-management/account-management/src/domain/mod.rsmodules/system/account-management/account-management/src/domain/tenant/repo.rsmodules/system/account-management/account-management/src/domain/tenant/service/mod.rsmodules/system/account-management/account-management/src/domain/tenant/service/service_tests.rsmodules/system/account-management/account-management/src/domain/tenant/test_support/auth.rsmodules/system/account-management/account-management/src/domain/tenant/test_support/repo.rsmodules/system/account-management/account-management/src/domain/user/service.rsmodules/system/account-management/account-management/src/domain/user/service_tests.rsmodules/system/account-management/account-management/src/domain/user_groups/cascade.rsmodules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rsmodules/system/account-management/account-management/src/domain/user_groups/mod.rsmodules/system/account-management/account-management/src/domain/user_groups/registration.rsmodules/system/account-management/account-management/src/domain/user_groups/registration_tests.rsmodules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rsmodules/system/account-management/account-management/src/infra/storage/entity/tenants.rsmodules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rsmodules/system/account-management/account-management/src/infra/storage/migrations/mod.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/metadata.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rsmodules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rsmodules/system/account-management/account-management/src/infra/types_registry/mod.rsmodules/system/account-management/account-management/src/lib.rsmodules/system/account-management/account-management/src/module.rsmodules/system/account-management/account-management/tests/metadata_integration.rsmodules/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
5bcb622 to
fc1466c
Compare
There was a problem hiding this comment.
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 winUpdate
deprovision_userdocs to include cleanup-originated errors.Line 421’s
# Errorslist is now incomplete:deprovision_usercan returnDomainError::ServiceUnavailable(RG cleanup timeout/transport failure) andDomainError::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 winMake the fake validate
ODataQueryto catch filter regressions.Line 64–70 currently ignores
_query, so tests won’t fail iffetch_tenant_groupsbuilds 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 winEmit dependency-health metric on cursor decode failures.
Line 331–333 returns
Retryablebut skips metric emission, unlike othercascade_list_groupsfailure 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 winAssert the outbound
list_groupsquery shape in the fake.
FakeMembershipRgClient::list_groupsignoresLine 788’s_query, so these tests won’t catch regressions that accidentally drop tenant/type filtering inlist_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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (53)
modules/system/account-management/account-management-sdk/Cargo.tomlmodules/system/account-management/account-management-sdk/src/gts.rsmodules/system/account-management/account-management-sdk/src/lib.rsmodules/system/account-management/account-management-sdk/src/metadata.rsmodules/system/account-management/account-management-sdk/src/metadata_tests.rsmodules/system/account-management/account-management/Cargo.tomlmodules/system/account-management/account-management/src/domain/conversion/repo.rsmodules/system/account-management/account-management/src/domain/conversion/service.rsmodules/system/account-management/account-management/src/domain/metadata/mod.rsmodules/system/account-management/account-management/src/domain/metadata/registry.rsmodules/system/account-management/account-management/src/domain/metadata/repo.rsmodules/system/account-management/account-management/src/domain/metadata/service.rsmodules/system/account-management/account-management/src/domain/metadata/service_tests.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/mod.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/repo.rsmodules/system/account-management/account-management/src/domain/metadata/test_support/repo_tests.rsmodules/system/account-management/account-management/src/domain/metrics.rsmodules/system/account-management/account-management/src/domain/mod.rsmodules/system/account-management/account-management/src/domain/ports/metrics.rsmodules/system/account-management/account-management/src/domain/ports/mod.rsmodules/system/account-management/account-management/src/domain/tenant/repo.rsmodules/system/account-management/account-management/src/domain/tenant/service/mod.rsmodules/system/account-management/account-management/src/domain/tenant/service/service_tests.rsmodules/system/account-management/account-management/src/domain/tenant/test_support/auth.rsmodules/system/account-management/account-management/src/domain/tenant/test_support/repo.rsmodules/system/account-management/account-management/src/domain/user/service.rsmodules/system/account-management/account-management/src/domain/user/service_tests.rsmodules/system/account-management/account-management/src/domain/user_groups/cascade.rsmodules/system/account-management/account-management/src/domain/user_groups/cascade_tests.rsmodules/system/account-management/account-management/src/domain/user_groups/mod.rsmodules/system/account-management/account-management/src/domain/user_groups/registration.rsmodules/system/account-management/account-management/src/domain/user_groups/registration_tests.rsmodules/system/account-management/account-management/src/infra/metrics.rsmodules/system/account-management/account-management/src/infra/metrics_tests.rsmodules/system/account-management/account-management/src/infra/mod.rsmodules/system/account-management/account-management/src/infra/storage/entity/conversion_requests.rsmodules/system/account-management/account-management/src/infra/storage/entity/tenants.rsmodules/system/account-management/account-management/src/infra/storage/migrations/m0005_tenant_metadata_indexes.rsmodules/system/account-management/account-management/src/infra/storage/migrations/mod.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/lifecycle.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/metadata.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/mod.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/reads.rsmodules/system/account-management/account-management/src/infra/storage/repo_impl/updates.rsmodules/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rsmodules/system/account-management/account-management/src/infra/types_registry/mod.rsmodules/system/account-management/account-management/src/lib.rsmodules/system/account-management/account-management/src/module.rsmodules/system/account-management/account-management/tests/metadata_integration.rsmodules/system/account-management/account-management/tests/metadata_integration_pg.rsmodules/system/resource-group/resource-group-sdk/src/api.rsmodules/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
8ba51a3 to
852005b
Compare
8db3e4c to
e10f5b9
Compare
…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>
e10f5b9 to
9c797be
Compare
* #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.
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>
eb331e4 to
4892b62
Compare
Summary
m0005, PG integration tests. User-groups cascade hook fires on tenant hard-delete before the IdP call.tenantsentity declaresresource_col = "id",TenantService::authorizeflips torequire_constraints(true)withOWNER_TENANT_ID+RESOURCE_ID, and the compiledAccessScopeis forwarded intoread_tenant/list_children/update_tenant/soft_delete. PolicyEnforcer advertisesCapability::TenantHierarchyso the PDP returns nativeInTenantSubtreerather than pre-resolvedIn.domain/ports/metrics.rswith seven segregated trait families (Bootstrap, Conversion, Dependency, Integrity, Metadata, Tenant, Storage) and a typed label-value taxonomy (every type annotated#[domain_model]per DE0309). Newinfra/metrics.rsholdsAmMetricsMeter(12 counters / 4 gauges / 1 histogram) implementing every port trait plus the transitionalMetricsFacadeBridge.domain/metrics.rsusesLazyLock<ArcSwap<Option<BridgeArc>>>soinstall_facade_bridgeis replaceable (meter-provider hot-swap in tests), andmodule.rsgates the install onmodkit::telemetry::metrics_provider_installed()so theNoopMeterProviderposture stays zero-cost on hot paths. Newmark_metrics_provider_installed()escape hatch for embedders that wire OTel outside modkit bootstrap (used byapi-gateway/tests/http_metrics_tests.rs).conversion_requestsentity declarestenant_col = "tenant_id"; service still passesallow_allto the conversion repo because dual-consent flows need barrier penetration on self-managed children.MetadataServicewalk-up usesallow_allfor ancestor reads (start tenant is PEP-authorized; ancestors are outside the caller's subtree by definition of the new clamp).Authorization story
tenantsScopable(no_*); service discarded compiled scope; repo always sawallow_alltenantsScopable(resource_col="id"); service forwards scope; secure-extension materialisestenants.id IN (SELECT descendant_id FROM tenant_closure WHERE ancestor_id = :root AND barrier = 0)JOINCompileFailed → CrossTenantDenied(HTTP 403) on missing-policytr-authz-pluginmasked by scope discardInTenantSubtree(RESOURCE_ID, …)or for the consumer to provide its own pluginObservability story
emit_metric/emit_gauge_value/emit_histogram_valueno-ops at call sitesMetricsFacadeBridgeonto OTel instruments per family constant&strliterals at every emit site&'static strnewtypes;From<&Failure>bridges delegate to SDKas_metric_label()— no duplicated variant→string mappingOnceLockbridge (first-wins forever) — stale instruments after meter-provider swapLazyLock<ArcSwap<Option<BridgeArc>>>— replace semantics; tests that hot-swap providers re-install bridges atomicallyopentelemetry.metrics.enabled = false—KeyValueallocations on every emitmodkit::telemetry::metrics_provider_installed();NoopMeterProviderposture keepsemit_*on silent no-op fast pathTest plan
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo 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 (exercisesmark_metrics_provider_installedescape hatch)conversion_integration10/10,lifecycle_integration12/12,metadata_integration5/5,repair_integration11/11cargo test -p cyberware-account-management --test conversion_integration_pg --test metadata_integration_pg(requires PG)Deployment note
AM now advertises
Capability::TenantHierarchyand resolves onlyRESOURCE_IDon thetenantsentity. Production deployment requires the PDP / AuthZ plugin chain to emitInTenantSubtree(RESOURCE_ID, …)(or a future plugin that respectsCapability::TenantHierarchy); the in-treetr-authz-plugincurrently emitsIn(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
docs/pr-review/modkit-rust-review.md+modkit-framework-compliance-review.mdfor the review checklist.constraint_bearing_enforcer(child)against an out-of-subtree target to assertNotFoundat the secure-extension layer.emit_*facade +MetricsFacadeBridgeare 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
Infrastructure Updates