Skip to content

Sparse ACLs: Identity Center permission-set scope bindings + Organizations hierarchy#129

Open
c1-squire-dev[bot] wants to merge 5 commits into
mainfrom
pquerna/sparse-acls
Open

Sparse ACLs: Identity Center permission-set scope bindings + Organizations hierarchy#129
c1-squire-dev[bot] wants to merge 5 commits into
mainfrom
pquerna/sparse-acls

Conversation

@c1-squire-dev

@c1-squire-dev c1-squire-dev Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Sparse ACLs (Cloud Infrastructure Access) for AWS Identity Center, in two phases:

  • Phase 1 reshapes the existing Identity Center permission-set assignment data into ConductorOne's Sparse-ACL ingest contract — a TRAIT_SCOPE_BINDING resource type carrying per-binding ScopeBindingTrait{role_id, scope_resource_id}, an "assigned" entitlement, a parent-pointer hierarchy edge, and Grant/Revoke that read the trait — so the AWS app ingests as SPARSE/HYBRID and lights up the RoleScopeBindingRelationship / JIT / by-inheritance UAR / JML pipelines.
  • Phase 2 adds the AWS Organizations Root → OU → Account hierarchy as scope tiers, so c1's by-inheritance review can walk the org tree above the account-leaf bindings.

Neither phase adds new mutation paths: all native integration (ListAccountAssignments, Create/DeleteAccountAssignment with status polling, conflict/suspended-account handling, group→member expansion) already existed and is reused unchanged. Phase 2 adds read-only Organizations tree calls.

What Phase 1 implements

  • permission_set role type (TRAIT_ROLE) — one resource per permission-set ARN; resource id is the bare ARN via the single shared helper permissionSetRoleID. pkg/connector/permission_set.go.
  • permission_set_assignment binding type (TRAIT_SCOPE_BINDING, OptInRequired) — one resource per (account, permission set) provisioned, carrying ScopeBindingTrait{role=permission_set, scope=account}, with the binding's parent set to the account (hierarchy edge for the by-inheritance ancestor walk). pkg/connector/permission_set_assignment.go.
  • "assigned" entitlement on the binding (slug exactly "assigned"), grantable to SSO users and groups.
  • Grants principal → binding → "assigned", reusing the existing per-assignment grant construction (direct grant for users; GrantExpandable{sso_group:<arn>:member} for groups — sparse and grant-expansion coexist).
  • Grant/Revoke read the ScopeBindingTrait (never parse the object id) to recover (account, permission set), then call a shared provisionAssignment / deprovisionAssignment core extracted from the existing account path — so AWS semantics (status verification, TargetType=AWS_ACCOUNT, status polling, idempotency annotations) are byte-identical.
  • account advertises ChildResourceType{permission_set_assignment} so the SDK crawls bindings under each account.
  • Registered both builders in the existing ssoEnabled && orgsEnabled block and in the default-capabilities builder; regenerated baton_capabilities.json (binding declares TRAIT_SCOPE_BINDING + CAPABILITY_PROVISION + optInRequired).

Binding object-id / round-trip decision (pre-implementation gate)

The binding object id is "<permissionSetArn>-<accountID>", built by the single helper permissionSetAssignmentObjectID, byte-identical to the platform's JIT-fabricated id (<role>-<scope>). The id is treated as an opaque external identity — it is never split back into (role, scope); role/scope are always recovered from the ScopeBindingTrait. A permission-set ARN embeds -, : and :::, but this is harmless: the type↔resource separator is :: and the resource half (the ARN + -<accountID>) is preserved verbatim by a first-:: split. Verified end-to-end and covered by unit tests.

HYBRID coexistence

The legacy account-entitlement Grant/Revoke path is retained (shared provision core), so the app is HYBRID during the cutover window and existing grants keep working. Removing the old account-entitlement path is deferred to a follow-up once HYBRID ingest is validated and in-flight grants are drained.

What Phase 2 implements (Organizations hierarchy)

Adds the org tree above the account-leaf bindings as navigation / by-inheritance review context — it does not add OU/Root-level grant targets. AWS Identity Center assigns permission sets to accounts only; there is no native OU- or Root-level assignment, so scope-bindings stay account-level (unchanged) and the new tiers exist purely so c1's by-inheritance UAR resolver can walk Account → OU → Root with the role pinned and the browse tree renders connected.

  • organization (root) resource type and organizational_unit resource type — scope tiers only (SkipEntitlementsAndGrants + OptInRequired), no traits, no bindings. pkg/connector/organization.go.
  • Org/OU builders walk ListRootsListOrganizationalUnitsForParent (recursive via ChildResourceType, paginated with pagination.Bag), parenting each OU to its crawl seed (root or parent OU).
  • account.List re-parents each active account onto its Root/OU via ListParents, wiring the parent_app_resource_* edge the ancestor walk needs. Flat ListAccounts streaming pagination is preserved — re-parenting is a per-account parent lookup, not a switch to per-OU listing.
  • Fail-soft, not fail-fast. Missing organizations:* read permission (e.g. existing IAM policies without the new perms) degrades gracefully to flat accounts with a single WARN rather than aborting the sync — bindings are account-leaf regardless, so the tree is genuinely optional context. Any other error propagates (no error-swallow / no false-revocation).
  • New org-read permissions declared on the relevant capability sets (organizations:ListParents on account; ListRoots / ListOrganizationalUnitsForParent on the org/OU types); registered the two builders in the ssoEnabled && orgsEnabled block and the default-capabilities builder; regenerated baton_capabilities.json.

Code-review fixes applied (Phase 1)

This PR has been revised to resolve the principal-engineer review:

  • R1 (blocking) — removed the V1Identifier that had been planted on the new permission_set role resource. A brand-new sparse-ACL resource type has no v1 predecessor entity, so there is no legacy id to preserve; this matches both proven exemplars (baton-confluence space_role, baton-azure-infrastructure role_assignment) and the sibling binding resource, which already omitted it.
  • T3 — Grant idempotency is now explicit. provisionAssignment pre-checks ListAccountAssignments and emits GrantAlreadyExists (skipping the create) when the assignment is already present, mirroring the existing GrantAlreadyRevoked-on-404 path in deprovisionAssignment, instead of relying on implicit CreateAccountAssignment server-side idempotency.
  • T1 — behavioral test coverage. Introduced a minimal ssoAdminAPI / orgsAPI client seam (the AWS SDK clients satisfy it structurally; production wiring is unchanged) and an in-memory fake, then added tests that exercise behavior, not just id shape: binding List emits the expected scope-binding resource + trait and is gated on an account parent; Grant calls CreateAccountAssignment with the right scope/role/principal; Revoke deletes the right assignment; and both idempotency annotations (GrantAlreadyExists / GrantAlreadyRevoked) fire.
  • T2 — round-trip test now pins the REAL contract. Replaced the circular local ResourceIDToString / ParseV2ExternalID replica with assertions against the real baton-sdk constructors (resource.NewResourceID + entitlement.NewEntitlementID). The type::resource external-id codec lives in the c1 platform repo (the SDK uses single-colon type:resource), which is intentionally outside a connector's module graph, so the end-to-end "::" byte-match is verified by the .c1z grep step against a live sync rather than an unimportable function — see the limitations below.
  • Pagination — verified, no change needed. The binding List collects all permission sets provisioned to an account by fully draining the SDK ListPermissionSetsProvisionedToAccount paginator inside getOrFetchPermissionSetIDs; there is no single-page truncation that could silently drop bindings. The binding Grants correctly passes the AWS assignment token through.

Testing done

  • go build ./... — success
  • go test ./... — all passing (23 sparse-ACL tests in pkg/connector, +10 new for the Phase 2 hierarchy)
  • golangci-lint run ./... — 0 issues
  • Phase 1 unit tests cover: object-id shape with a real ARN; byte-match to the JIT fabrication; role-id drift guard between the role resource and the trait; trait byte-match (role/scope ids + parent edge); "assigned" slug + entitlement id; real-SDK reconcile-key pinning (NewResourceID / NewEntitlementID); and behavioral List/Grant/Revoke/idempotency tests via the faked client.
  • Phase 2 unit tests (via the same client-mock seam): org-root emission, OU crawl under a root and recursively under a parent OU, parent-type gating (OUs never listed top-level or under a non-root/OU parent), account re-parenting to Root vs OU, and the organizations:* access-denied fail-soft paths (org/OU builders and account re-parenting all degrade without error).
  • Regenerated baton_capabilities.json verified to declare the new types with the correct traits/capabilities (purely additive).

Deferred to later phases / known limitations

  • Account-leaf scope ceiling (product implication). Unlike Azure mgmt-group cascade, an OU/Root node can never hold a binding — every grant resolves to a concrete account. The OU/Root tiers appear in review/browse and enable by-inheritance roll-up, but a "grant on OU-Eng" request has no native realization and would have to fan out to per-account grants (a future enhancement).
  • Org hierarchy opt-in vs re-parenting. The organization / organizational_unit types are OptInRequired. Account re-parenting is gated on the organizations:ListParents permission (fail-soft), not on opt-in state (the SDK does not expose opted-in resource types to a builder). A deployment that grants the perm but does not opt into the org/OU types could produce account parent pointers to un-synced tiers; c1's ancestor walk is gap-tolerant, but enabling the hierarchy types alongside the perm is recommended.
  • Double representation during the HYBRID overlap window. With the legacy path retained, the same underlying AWS account assignment is emitted twice — once as a classic account-entitlement grant and once as the sparse binding "assigned" grant. This is intentional per the cutover strategy and harmless in c1 (distinct resource types), but operators will see each access in both models until the legacy account-entitlement path is removed in a follow-up (after HYBRID validation + grant drain).
  • End-to-end .c1z byte-match verification (c1-side) outstanding. The connector-side bytes (binding object id, trait role/scope ids, "assigned" entitlement id, account/OU/Root parent edges) are fully unit-tested, but the JIT↔sync reconcile byte-match against live c1 (scope_role_jit.go <role>-<scope> shape) and the by-inheritance walk over a real org tree should be confirmed once via a .c1z round-trip on a real Identity Center org before enabling on a tenant.
  • Phase 3 — classic IAM feasibility (expected out of scope; effective scope lives in policy documents, not a grantable (role, scope) binding).

This ships behind OptInRequired (opt in via --sync-resource-types), so it is dark until enabled per tenant.

pquerna and others added 2 commits June 17, 2026 06:27
Reshape Identity Center permission-set assignments into the c1 Sparse-ACL
contract: a permission_set role type, a permission_set_assignment binding type
carrying TRAIT_SCOPE_BINDING with ScopeBindingTrait{role,scope}, an "assigned"
entitlement, account ChildResourceType wiring, and trait-reading Grant/Revoke
that reuse the existing Create/DeleteAccountAssignment core (HYBRID coexistence:
the legacy account-entitlement path is retained).

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Add unit tests for the §3.6 gate invariants: object-id shape with a real
permission-set ARN, byte-match to c1's JIT fabrication, role-id drift guard
between the role resource and the trait, trait byte-match, the "assigned" slug,
and the ResourceIDToString/ParseV2ExternalID round-trip. Regenerate
baton_capabilities.json to declare permission_set (TRAIT_ROLE) and
permission_set_assignment (TRAIT_SCOPE_BINDING + CAPABILITY_PROVISION, optInRequired).

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/permission_set_assignment.go
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Connector PR Review: Sparse ACLs: Identity Center permission-set scope bindings + Organizations hierarchy

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base ae20289.
Review mode: incremental since ce8e0f6
View review run: https://github.com/ConductorOne/baton-aws/actions/runs/27979615160

Review Summary
The full PR diff was scanned for security and correctness; no secrets, weak crypto, insecure TLS, injection, or command execution were found. The new commit refactors permissionSetAssignmentResourceType.List to paginate the per-account permission-set fan-out in entitlementsBatchSize batches, reusing the same entitlementsPageState plus encode/decodePageToken mechanism already used and tested by account.Entitlements. Termination, batch boundaries, and one-binding-per-permission-set are verified by the added TestPermissionSetAssignmentList_PaginatesInBatches; no new issues found.

Security Issues
None found.

Correctness Issues
None found.

Suggestions
None.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

…cy, behavioral tests, real-SDK reconcile keys

R1: remove V1Identifier from the new permission_set role resource (brand-new
sparse-ACL type has no v1 predecessor; matches confluence/azure exemplars and
the sibling binding resource).

T3: provisionAssignment now pre-checks ListAccountAssignments and emits
GrantAlreadyExists (mirroring the existing GrantAlreadyRevoked-on-404 path)
instead of relying on implicit CreateAccountAssignment idempotency.

T1: add behavioral unit tests via a new ssoAdminAPI/orgsAPI client seam and an
in-memory fake — binding List emits the scope-binding resource+trait, Grant
calls CreateAccountAssignment with the right scope/role/principal, Revoke
deletes the right assignment, and both idempotency annotations fire.

T2: the reconcile-key test now pins against the REAL baton-sdk constructors
(resource.NewResourceID + entitlement.NewEntitlementID); removed the circular
local ResourceIDToString/ParseV2ExternalID replica (those are c1 platform
internals, not in the connector module graph — end-to-end "::" round-trip is
covered by the .c1z grep step).

Pagination: confirmed binding List paginates fully via the SDK paginator in
getOrFetchPermissionSetIDs (no single-page truncation); no change needed.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
@c1-squire-dev c1-squire-dev Bot marked this pull request as ready for review June 17, 2026 06:58
@c1-squire-dev c1-squire-dev Bot requested a review from a team June 17, 2026 06:58

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

Add the AWS Organizations org tree as Sparse ACLs scope context so c1's
by-inheritance review walks Account → OU → Root with the role pinned.

- New `organization` (root) and `organizational_unit` resource types: scope
  tiers only (SkipEntitlementsAndGrants + OptInRequired), no bindings — AWS
  Identity Center assigns permission sets to ACCOUNTS only, so there is no
  native OU/Root-level assignment. The tiers are hierarchy/review context.
- New org/OU builders walk ListRoots → ListOrganizationalUnitsForParent
  (recursive, via ChildResourceType) with pagination.Bag, parenting each OU
  to its crawl seed.
- account.List re-parents each account onto its Root/OU via ListParents,
  wiring the hierarchy edge the by-inheritance ancestor walk needs.
- Fail-soft: missing organizations:* read permission degrades to flat
  accounts with a WARN rather than aborting the sync (errors otherwise
  propagate — no false-revocation). New org-read perms declared on the
  account / org / OU capability sets; baton_capabilities.json regenerated.
- Behavioral tests via the existing client-mock seam: root/OU emission,
  nested-OU recursion, parent-type gating, account re-parenting, and the
  access-denied fail-soft paths.

Scope-bindings stay account-level and the TRAIT_SCOPE_BINDING contract is
unchanged; this commit only adds hierarchy context above the binding.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
@c1-squire-dev c1-squire-dev Bot changed the title Sparse ACLs Phase 1: Identity Center permission-set scope bindings Sparse ACLs: Identity Center permission-set scope bindings + Organizations hierarchy Jun 17, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

permissionSetAssignmentResourceType.List buffered every provisioned
permission set for an account into a single response with no page token,
doing one DescribePermissionSet per set synchronously — unlike
account.Entitlements, which batches via entitlementsBatchSize. For accounts
with many provisioned permission sets this is a large synchronous fan-out
per call.

Reuse the existing account-entitlement batching mechanism verbatim
(entitlementsPageState + entitlementsBatchSize + encode/decodePageToken) so
binding List paginates identically to account.Entitlements: at most one
batch of bindings per call, with a page token resuming at the next index.
Keeps the rate-limit blast radius small enough to resume from a checkpoint
(D-SACL-007 fidelity — no new paging mechanism).

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant