Skip to content

Bug-fix batch: api-gateway CORS expose-headers, AM listing / IdP errors / conversions / child counts, resource-group PEP type#4226

Merged
Artifizer merged 7 commits into
constructorfabric:mainfrom
diffora:fix/am-gateway-rg-batch
Jul 18, 2026
Merged

Bug-fix batch: api-gateway CORS expose-headers, AM listing / IdP errors / conversions / child counts, resource-group PEP type#4226
Artifizer merged 7 commits into
constructorfabric:mainfrom
diffora:fix/am-gateway-rg-batch

Conversation

@diffora

@diffora diffora commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

A batch of independent bug fixes, one commit per fix, spanning api-gateway, account-management (+SDK), resource-group, and toolkit-db.

CORS-expose the ETag response header (api-gateway)

Browsers may only read non-safelisted response headers when the server sends Access-Control-Expose-Headers; the gateway sent none, so every ETag-guarded (If-Match) write was impossible for a cross-origin browser client while curl/Postman hid the problem. Adds cors.exposed_headers (default ["ETag"], so deployments need no config change), config-load validation for wildcard+credentials combinations (previously a tower-http assert panic at startup — a crash-loop with no pointer at the offending config), and warnings for unparseable CORS list entries instead of a silent filter_map drop.

$orderby=status on the tenant children listing (account-management + toolkit-db + resource-group)

Sorting the tenant children listing by status returned 400 unsupported $orderby field. Root cause: the cursor codec was bound to the wire String kind while the cursor value carries the storage SMALLINT ordinal, so the token could not round-trip. Adds ODataFieldMapping::cursor_kind (default = the wire kind, so no existing mapper changes behavior) and uses it on both sides of the cursor codec; encode_cursor_value also accepts SmallInt/Int under FieldKind::I64 so mappers need not hand-widen narrow columns. status becomes orderable by its lifecycle ordinal (active < suspended < deleted). resource-group's three type fields carried the same latent wire-String/storage-SmallInt mismatch (orderable, un-overridden — $orderby=type broke with InvalidCursor on any page overflow) and now declare cursor_kind = I64. OpenAPI updated accordingly.

Typed IdP uniqueness / password-policy rejections (account-management SDK + gear)

POST /tenants/{id}/users with a duplicate username surfaced as a redacted generic 400 (request/VALIDATION), so clients could not attribute the failure to a field; duplicate email and password-policy rejections collapsed into the same catch-all. The #[non_exhaustive] IdpUserOperationFailure gains DuplicateUser { field: Username | Email | UsernameOrEmail } and PasswordPolicy; Rejected stays as the fallback, so providers that never emit the new variants keep today's behavior unchanged. AM maps DuplicateUser to 409 already_exists on the user resource (the stable colliding-field token as resource_name, never the caller-supplied value; the curated detail derives from the same typed field) and PasswordPolicy to 400 with a password/PASSWORD_POLICY field violation. UsernameOrEmail covers Keycloak's combined ModelDuplicateException constant — on KC 26 the only 409 shape createUser produces directly. Raw provider text stays digest-only in logs, same redaction posture as the existing arms. The IdP-plugin half that actually emits the classified variants lives in the consuming application; until it lands, every path still produces Rejected and nothing changes on the wire.

Soft-delete cancels the pending conversion in-TX (account-management)

DELETE /tenants/{id} tombstoned the tenant but left its pending mode-conversion row dangling. schedule_deletion's SERIALIZABLE TX now terminal-resolves it (cancelled_by = the deleting actor); the idempotent already-Deleted path runs the same fenced cancel, so pre-fix orphans are healed by simply retrying the DELETE. The audit event is emitted after commit (no double-count on serialization retry, no phantom event on rollback).

Platform-scope the user-groups cascade system actor (account-management)

The retention hard-delete reaper's resource-group cascade ran scoped to the tenant being deleted; the resolver's subtree grant rooted at a Deleted tenant materializes to zero rows and fails closed, silently deferring the whole hard-delete backlog forever. The cascade context is now platform-scoped (nil home tenant resolves to Global at the resolver). The resolver-side half lives in the consuming application.

Count self_managed direct children in child_count (account-management)

The public child_count read-shape field undercounted a parent whose direct children include a self_managed one — a parent with two active children where one is self_managed reported child_count = 1 while GET /tenants/{id}/children listed both. Root cause: count_children_grouped clamped each child row by the caller's barrier-respecting scope, and a self_managed tenant's inbound closure edges carry barrier = 1 even from its own parent, so the self_managed direct child dropped out of the count while the listing (direct-child carve-out) still showed it. The count now gates on the PARENT's Respect-reachability and then counts all its direct children; the parent_id predicate pins the count to depth 1, so nothing below a barrier leaks. Regression tests run under a real Respect-barriers scope (prior coverage ran under allow_all, barriers off, and could not catch this).

Restore rg-namespace group resource type for PEP evaluation (resource-group)

The gts-rust v0.11.0 migration routed the group resource-type literal through the crate-name-derived SDK constant, silently renaming the PEP-evaluated type from gts.cf.core.rg.group.v1~ to gts.cf.core.resource_group.group.v1~. Deployed role grants are written against the documented gts.cf.core.rg.* family, so the rename 403'd all group CRUD for tenant members and failed AM's tenant-delete ownership probe closed (surfaced as 503) — verified on a live deployment: a grant on gts.cf.core.rg.* is denied while gts.cf.core.resource_group.* is allowed, same caller and flow. Pins GROUP_RESOURCE_TYPE back to cf.core.rg.group.v1~, consistent with the untouched siblings (rg.group_membership.v1~, rg.type.v1~), with a comment so a future mechanical migration does not rename it again.

Note for maintainers: if crate-derived GTS namespaces are genuinely the intended direction, that should be a separate coordinated migration — the whole type family at once, plus seed data and a data migration of existing grants — not a mechanical rewrite of a single literal (which is exactly how this regression happened).

Verification

Branch is rebased onto current main. cargo test / cargo clippy --all-targets / cargo fmt --check are green across the six touched crates (toolkit-db, api-gateway, account-management, account-management-sdk, resource-group, resource-group-sdk); cargo dylint --all (incl. the GTS-layer lints DE0901/DE0904) green on the resource-group crates. The AM, gateway, and resource-group fixes were additionally exercised end-to-end against a live deployment (REST-level checks for the CORS header, $orderby=status incl. a cursor walk across ordinal groups, the duplicate/password-policy classification, the delete-cancels-conversion flow, and the self_managed child_count agreement between the single read and the listing).

Summary by CodeRabbit

  • New Features

    • Added clearer account-management errors for duplicate users and password-policy rejections, with sensitive provider details redacted.
    • Tenant deletion now automatically cancels pending conversion requests and records the responsible actor.
    • Tenant child listings support ordering and cursor pagination by lifecycle status.
    • CORS now exposes ETag by default and supports configurable exposed headers.
  • Bug Fixes

    • Improved scoped child counts to include eligible direct children without exposing deeper subtrees.
    • Corrected resource-type identifiers and cursor handling for resource-group listings.
  • Documentation

    • Clarified filtering and ordering behavior for categorical fields.

diffora added 2 commits July 18, 2026 10:53
The RBAC/AM write contract is ETag-guarded: DELETE/PATCH on role
assignments and role definitions require the resource's current ETag in
If-Match, and the only way to obtain it is to read the ETag header from a
GET/POST response. Browsers may only read non-safelisted response headers
when the server sends Access-Control-Expose-Headers, and the gateway sent
none at all - so every cross-origin browser client saw ETag as absent and
every ETag-guarded write dead-ended, while curl/Postman hid the problem.

Add CorsConfig.exposed_headers (wired to CorsLayer::expose_headers,
"*" supported like the sibling lists) defaulting to ["ETag"] so
deployments get the fix without a config change.

Also harden CORS config handling, both surfaced while reviewing the fix:
- validate_cors_config at gear init rejects any wildcard CORS list
  combined with allow_credentials=true. tower-http asserts on exactly
  this inside Layer::layer, and axum's eager router layering turned it
  into a startup crash-loop with no pointer at the config; now it is a
  clean init error naming the offending list.
- Unparseable entries in ANY cors list warn and are skipped instead of
  vanishing in a silent filter_map: a typo'd exposed_headers entry used
  to silently suppress Access-Control-Expose-Headers and re-break every
  ETag-guarded browser write. A shared parse_list helper replaces the
  four copies of the parse pattern.

Covers the default and an explicitly configured list with request-level
tests, plus unit tests for each validation arm.

Signed-off-by: Diffora <ddiffora@gmail.com>
…ren listing

Clicking the Status column on the Tenants page issued
GET /tenants/{id}/children?$orderby=status and got 400
'unsupported $orderby field: status': the field was flagged
non-orderable because the cursor codec was hard-wired to the wire
filter kind (String) while the extracted cursor value speaks the
storage shape (SMALLINT) - encode/parse could not round-trip the token.

Give ODataFieldMapping a per-field cursor_kind() (default: the wire
field.kind(), so every existing mapper is untouched) and use it on both
sides of the cursor codec. encode_cursor_value also accepts SmallInt/Int
under FieldKind::I64 so mappers exposing narrow integer columns need not
hand-widen to BigInt. The AM mapper overrides Status -> I64 and extracts
the storage ordinal, so status becomes orderable by its lifecycle ordinal
(active < suspended < deleted) with cursor pages comparing integer to
SMALLINT - a numeric SQL comparison.

tenant_type stays non-orderable (ordering by a derived UUIDv5 has no
honest meaning). Filter comparisons on status stay membership-only
(eq/ne/in).

resource-group carried the same latent wire-String/storage-SMALLINT
mismatch on its three type fields (Group/Hierarchy/Membership), orderable
and un-overridden - $orderby=type broke with InvalidCursor on any page
overflow; they now declare cursor_kind=I64.

Integration tests replace the old rejection pin with a lifecycle-ordinal
sort assertion and a full cursor walk across ordinal groups (bounded
against a repeated-cursor hang). Codec unit tests pin the narrow-integer
round-trip and the loud failure for a genuine wire/storage mismatch.
OpenAPI documents $orderby=status as supported with lifecycle-ordinal
semantics.

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

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@diffora, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4dbf1a5-8608-48b2-8775-40d495916127

📥 Commits

Reviewing files that changed from the base of the PR and between 3f105e2 and f003271.

📒 Files selected for processing (29)
  • gears/system/account-management/account-management-sdk/src/field.rs
  • gears/system/account-management/account-management-sdk/src/idp_user.rs
  • gears/system/account-management/account-management-sdk/src/lib.rs
  • gears/system/account-management/account-management/src/domain/error.rs
  • gears/system/account-management/account-management/src/domain/error_tests.rs
  • gears/system/account-management/account-management/src/domain/idp/idp_tests.rs
  • gears/system/account-management/account-management/src/domain/idp/mod.rs
  • gears/system/account-management/account-management/src/domain/system_actor.rs
  • gears/system/account-management/account-management/src/domain/tenant/repo.rs
  • gears/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • gears/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • gears/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • gears/system/account-management/account-management/src/infra/sdk_error_mapping.rs
  • gears/system/account-management/account-management/src/infra/sdk_error_mapping_tests.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • gears/system/account-management/account-management/tests/conversion_integration.rs
  • gears/system/account-management/account-management/tests/lifecycle_integration.rs
  • gears/system/account-management/account-management/tests/list_children_integration.rs
  • gears/system/account-management/account-management/tests/metadata_integration.rs
  • gears/system/account-management/account-management/tests/metadata_integration_pg.rs
  • gears/system/account-management/account-management/tests/updates_integration.rs
  • gears/system/account-management/docs/account-management-v1.yaml
  • gears/system/resource-group/resource-group-sdk/src/error_tests.rs
  • gears/system/resource-group/resource-group-sdk/src/gts.rs
  • gears/system/resource-group/resource-group/src/api/rest/error.rs
  • gears/system/resource-group/resource-group/tests/domain_unit_test.rs
📝 Walkthrough

Walkthrough

The changes add typed IdP duplicate and password-policy errors, transactional conversion cancellation during tenant deletion, platform-scoped cascade authorization, storage-aware OData cursors, CORS validation and exposed headers, and the canonical resource-group GTS identifier.

Changes

Account error taxonomy

Layer / File(s) Summary
Typed error contracts and canonical mapping
gears/system/account-management/account-management-sdk/src/*, gears/system/account-management/account-management/src/domain/*, gears/system/account-management/account-management/src/infra/sdk_error_mapping*
Adds duplicate-field and password-policy classifications, redacts provider detail, and maps them to stable canonical error envelopes with regression tests.

Tenant deletion lifecycle

Layer / File(s) Summary
Deletion actor and authorization scope
gears/system/account-management/account-management/src/domain/system_actor.rs, .../domain/tenant/*
Passes deleted_by through tenant deletion and makes deleted-tenant user-group cascades platform-scoped.
Conversion cancellation
gears/system/account-management/account-management/src/infra/storage/repo_impl/*, .../tests/conversion_integration.rs
Cancels pending conversions in the deletion transaction, preserves cancellation metadata on retries, and emits an event when rows are cancelled.

OData pagination and resource-group identity

Layer / File(s) Summary
Storage-shaped cursors and tenant queries
libs/toolkit-db/src/odata/*, gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs, .../tests/list_children_integration.rs
Adds mapper-controlled cursor kinds, enables status ordering, supports narrow integer cursor values, and corrects scoped child counting.
Resource-group mappings
gears/system/resource-group/resource-group/src/infra/storage/odata_mapper.rs, gears/system/resource-group/resource-group-sdk/src/*
Applies storage-shaped cursor kinds and updates the canonical group GTS namespace.

CORS configuration

Layer / File(s) Summary
Validation and response headers
gears/system/api-gateway/src/config.rs, .../src/cors.rs, .../src/gear.rs, .../tests/cors_tests.rs
Adds configurable exposed headers with an ETag default, rejects invalid wildcard credential combinations during startup, warns on invalid entries, and verifies response headers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main batch of changes across CORS, account-management, and resource-group.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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

🤖 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 `@gears/system/account-management/account-management-sdk/src/field.rs`:
- Around line 48-53: Add a PasswordPolicy variant to ValidationReason and map it
to and from the PASSWORD_POLICY constant, preserving Unknown handling for
unrecognized values. Extend the existing ValidationReason round-trip test to
verify PASSWORD_POLICY converts to PasswordPolicy and back.

In
`@gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs`:
- Around line 501-537: The parent reachability gate and child count in the
affected read method must execute atomically rather than as separate queries.
Replace the preliminary `reachable` query and subsequent `allow_all` count with
one SQL statement that joins or subqueries Respect-scoped parents while counting
their direct children, preserving the existing exclusions and empty-result
behavior.
- Around line 458-480: Update the public child_count contract in
account-management-v1.yaml to document the parent-gated direct-child carve-out:
count self_managed direct children when their parent is Respect-reachable, while
excluding descendants behind an impenetrable self-managed barrier. Replace the
outdated statement that such direct children are never counted, and keep the
existing behavior for non-Respect-reachable parents.
🪄 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

Run ID: 93facb16-3700-4a4b-9405-33e45d73ea20

📥 Commits

Reviewing files that changed from the base of the PR and between 7082216 and 3f105e2.

📒 Files selected for processing (36)
  • gears/system/account-management/account-management-sdk/src/field.rs
  • gears/system/account-management/account-management-sdk/src/idp_user.rs
  • gears/system/account-management/account-management-sdk/src/lib.rs
  • gears/system/account-management/account-management-sdk/src/tenant.rs
  • gears/system/account-management/account-management/src/domain/error.rs
  • gears/system/account-management/account-management/src/domain/error_tests.rs
  • gears/system/account-management/account-management/src/domain/idp/idp_tests.rs
  • gears/system/account-management/account-management/src/domain/idp/mod.rs
  • gears/system/account-management/account-management/src/domain/system_actor.rs
  • gears/system/account-management/account-management/src/domain/tenant/repo.rs
  • gears/system/account-management/account-management/src/domain/tenant/service/mod.rs
  • gears/system/account-management/account-management/src/domain/tenant/service/service_tests.rs
  • gears/system/account-management/account-management/src/domain/tenant/test_support/repo.rs
  • gears/system/account-management/account-management/src/infra/sdk_error_mapping.rs
  • gears/system/account-management/account-management/src/infra/sdk_error_mapping_tests.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/conversion.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/mod.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs
  • gears/system/account-management/account-management/src/infra/storage/repo_impl/updates.rs
  • gears/system/account-management/account-management/tests/conversion_integration.rs
  • gears/system/account-management/account-management/tests/lifecycle_integration.rs
  • gears/system/account-management/account-management/tests/list_children_integration.rs
  • gears/system/account-management/account-management/tests/metadata_integration.rs
  • gears/system/account-management/account-management/tests/metadata_integration_pg.rs
  • gears/system/account-management/account-management/tests/updates_integration.rs
  • gears/system/account-management/docs/account-management-v1.yaml
  • gears/system/api-gateway/src/config.rs
  • gears/system/api-gateway/src/cors.rs
  • gears/system/api-gateway/src/gear.rs
  • gears/system/api-gateway/tests/cors_tests.rs
  • gears/system/resource-group/resource-group-sdk/src/error_tests.rs
  • gears/system/resource-group/resource-group-sdk/src/gts.rs
  • gears/system/resource-group/resource-group/src/api/rest/error.rs
  • gears/system/resource-group/resource-group/src/infra/storage/odata_mapper.rs
  • gears/system/resource-group/resource-group/tests/domain_unit_test.rs
  • libs/toolkit-db/src/odata/sea_orm_filter.rs

Comment thread gears/system/account-management/account-management-sdk/src/field.rs
diffora added 5 commits July 18, 2026 11:20
… rejections

POST /tenants/{id}/users with a duplicate username came back as the
redacted generic 400 invalid_argument (field=request, reason=VALIDATION,
'identity provider rejected the user operation (detail redacted; ...)').
The client could not attribute the failure to the username field, which
blocks the 'Add user dialogs - per-field and API error handling'
acceptance criterion. Duplicate email and password-policy rejections
collapsed into the same catch-all.

SDK: extend the #[non_exhaustive] IdpUserOperationFailure with typed
variants - DuplicateUser { field: IdpUserDuplicateField (Username |
Email | UsernameOrEmail), detail } and PasswordPolicy { detail };
Rejected stays as the fallback for unattributable rejections, so legacy
plugins keep today's behavior unchanged. UsernameOrEmail covers
Keycloak's ModelDuplicateException path, which emits the combined
constant 'User exists with same username or email' (on KC 26 the only
409 createUser produces directly) and must not be misattributed to
username. New field/reason vocabulary: PASSWORD_FIELD, PASSWORD_POLICY.

AM: map DuplicateUser to DomainError::UserAlreadyExists -> HTTP 409
already_exists on the user resource type, carrying the typed field so
the canonical boundary derives both resource_name (the stable colliding
-field token 'username'/'email', never the caller-supplied value) and
the curated detail from one place; map PasswordPolicy to
DomainError::IdpPasswordPolicy -> HTTP 400 with field_violations
[{field: password, reason: PASSWORD_POLICY}] instead of the generic
request/VALIDATION pair. Raw provider text stays digest-only in the
am.idp log lines, same redaction posture as the existing arms.

The vp-idp-plugin change that actually emits the classified variants
from Keycloak 409/400 responses lands separately after the submodule
bump - until then every path still produces Rejected and nothing
changes on the wire.

Signed-off-by: Diffora <ddiffora@gmail.com>
…-delete TX

DELETE /tenants/{id} tombstoned the tenant but left any pending
mode-conversion request row dangling. schedule_deletion's SERIALIZABLE
TX now terminal-resolves the pending conversion (cancelled_by = the
deleting actor) inside the same transaction, so a committed soft-delete
never leaves an orphaned conversion behind.

The already-Deleted idempotent short-circuit also runs the same
(Pending-fenced) cancel cascade, so tenants soft-deleted BEFORE the
cascade existed are healed by the natural operator remediation -
retrying the DELETE; post-fix tombstones make it a no-op.

The conversion auto-cancel am.events line is emitted AFTER the
SERIALIZABLE TX commits (the count rides the closure return): logging
inside the closure would double-count on a 40001 retry and emit a
phantom event when the TX ultimately rolled back.

Signed-off-by: Diffora <ddiffora@gmail.com>
…m actor

The retention hard-delete reaper's resource-group cascade ran scoped to
the tenant being deleted. The resolver's subtree grant is rooted at that
tenant, but the tenant row is already Deleted, so the grant materializes
to zero rows and the PDP fails closed - the cascade is denied and the
whole hard-delete backlog is deferred forever, silently.

Platform-scope the cascade's system-actor context (a nil home tenant
resolves to Global at the resolver) so the reaper can clean up
resource-group state for tenants it is tearing down. The resolver-side
halves land separately after the submodule bump.

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

The gts-rust v0.11.0 migration routed the group resource-type literal
through the crate-name-derived SDK constant, which silently renamed the
PEP-evaluated type from gts.cf.core.rg.group.v1~ to
gts.cf.core.resource_group.group.v1~. Every deployed role grant is
written against the documented gts.cf.core.rg.* family, so the rename
403'd all group CRUD for tenant members and failed AM's tenant-delete
ownership probe closed (surfaced as 503) - verified live: a grant on
gts.cf.core.rg.* is denied while gts.cf.core.resource_group.* is
allowed, on the same caller and flow.

Pin GROUP_RESOURCE_TYPE back to cf.core.rg.group.v1~, consistent with
the untouched siblings (rg.group_membership.v1~, rg.type.v1~), and
realign the #[resource_error] tags + tests that the SDK round-trip
tests pin to the constant. A code comment marks the literal as the
PEP-evaluated type that must NOT be crate-derived, so a future
mechanical migration does not silently rename it again.

Adopting crate-derived namespaces intentionally would be a separate,
coordinated migration: the whole type family at once, plus installer
seeds and a data migration of existing grants.

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

The scope-selector counter on the Tenants page (the public `child_count`
read-shape field) undercounted a parent whose direct children include a
`self_managed` one: a parent with two active children where one is
self_managed reported `child_count = 1`, while `GET /tenants/{id}/children`
correctly listed both. Operator UIs render the wrong direct-child count.

Root cause: `count_children_grouped` clamped each child row by the
caller's barrier-respecting scope (`id IN (closure WHERE ancestor = root
AND barrier = 0)`). A self_managed tenant's inbound closure edges carry
`barrier = 1` even from its own parent, so a Respect-reachable parent's
self_managed direct child was dropped from the count — even though the
identity-level direct-child carve-out (see `service::scope_util`) makes
it visible through `list_children`. The two read paths disagreed.

Gate the count on the PARENT's Respect-reachability instead of each
child's own barrier-clamped visibility: resolve which requested parents
the caller can Respect-reach, then count all their direct children
(the `parent_id` predicate pins the count to depth 1, so nothing below a
barrier leaks — a parent reachable only via the carve-out, i.e. a
self_managed tenant past its barrier, is absent from the Respect read
and its subtree count stays 0). `Provisioning` excluded, `Deleted`
included, as before.

Regression tests exercise a real Respect-barriers `InTenantSubtree`
scope (the previous coverage ran under `allow_all`, with barriers off,
and could not catch this): one asserts a reachable parent's count
includes its self_managed direct child, one asserts a past-barrier
parent's subtree stays uncounted.

Signed-off-by: Diffora <ddiffora@gmail.com>
@diffora
diffora force-pushed the fix/am-gateway-rg-batch branch from 3f105e2 to f003271 Compare July 18, 2026 09:20
@diffora

diffora commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

All three review findings addressed and folded into their originating commits (history stays one-commit-per-fix):

  • ValidationReason gains a PasswordPolicy variant wired into from_wire/as_wire and the round-trip test — the new reason no longer decodes as Unknown (folded into the IdP-classification commit).
  • account-management-v1.yaml child_count description now documents the parent-gated semantics: all direct children (incl. self_managed) counted when the parent is Respect-reachable, 0 for a tenant seen only via the carve-out (folded into the child_count commit).
  • count_children_grouped is now a single statement: the Respect-scoped parent gate rides as an IN-subquery inside the grouped count, so the authorization check and the count observe one database snapshot — no gate-then-count race (folded into the child_count commit).

Tests, fmt, clippy green on the touched crates; branch force-pushed.

@Artifizer
Artifizer merged commit cf78b7b into constructorfabric:main Jul 18, 2026
27 checks passed
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.

2 participants