Skip to content

feat: initial Bonbon (Account Access) connector scaffold#120

Draft
btipling wants to merge 1 commit into
mainfrom
bt/bonbon-scaffold
Draft

feat: initial Bonbon (Account Access) connector scaffold#120
btipling wants to merge 1 commit into
mainfrom
bt/bonbon-scaffold

Conversation

@btipling

Copy link
Copy Markdown
Contributor

Summary

Scaffolds the AWS Account Access (codename Bonbon) connector as an extension to baton-aws. Gated behind --global-bonbon-enabled; existing baton-aws syncs are unchanged when the flag is off.

  • Hand-rolled REST + SigV4 client under pkg/connector/bonbon/client/ (Bonbon is not in aws-sdk-go-v2 yet). All 11 operations from the AWS service-2.json are exposed as typed Go methods. Signing uses github.com/aws/aws-sdk-go-v2/aws/signer/v4 directly, sourced from the existing baton-aws AWS credential chain.
  • Three resource types — bonbon_application, bonbon_role, and grants on bonbon_role for IdC user / IdC group principals. All annotated with v2.OptInRequired{} since Bonbon is private preview, plus v2.CapabilityPermissions declaring the account-access:* actions per resource type.
  • test/bonbon-testserver/ — in-memory mock of the 11 routes for unit tests without a real Bonbon-enabled account.
  • docs/bonbon.md — customer onboarding runbook covering Org-wide vs standalone enablement, the required trust policy with sts:AssumeRole + sts:SetContext, IAM permissions, and the private-preview limitations from the AWS onboarding guide.

Background and decisions (path-selection rationale, AWS SDK availability, region scoping, opt-in semantics) live in the design plan at arena-fs /engineer/baton-bonbon-plan.md.

Refs: CXH-1558

Status

DRAFT. Scaffold compiles and go test ./pkg/connector/bonbon/... passes. TestBonbonFullSync is t.Skip()'d — the testserver loopback auth path needs finalization before the full sync round-trip can be asserted in unit tests.

Test plan

  • go build ./... clean
  • go test ./pkg/connector/bonbon/... passes (TestValidateRegion)
  • Wire TestBonbonFullSync against the testserver — follow-up PR
  • Manual smoke against a real Bonbon-enabled AWS account in us-east-1:
    • Validate() returns success with the trust policy attached
    • CreateEntitlement + assume against an IdC user
    • DeleteEntitlement removes the binding

🤖 Generated with Claude Code

Adds the private-preview Bonbon connector under
pkg/connector/bonbon/ alongside the existing baton-aws resource
builders, gated by --global-bonbon-enabled.

The Bonbon service (account-access-preview.amazonaws.com) is not
yet in aws-sdk-go-v2, so this introduces a hand-rolled REST/SigV4
client under pkg/connector/bonbon/client/. The 11 operations from
the AWS service-2.json are exposed as typed Go methods. Signing
uses github.com/aws/aws-sdk-go-v2/aws/signer/v4 directly, sourced
from the existing baton-aws AWS credential chain so AssumeRole +
external-id flows are inherited as-is.

Resource model:
  - bonbon_application — one per Bonbon Application (IdC binding)
  - bonbon_role        — one per target IAM Role ARN seen across
                         entitlements
  - Grants on bonbon_role: assigned-to IdC user (sso_user) or IdC
                          group (sso_group). Grant = CreateEntitlement,
                          Revoke = DeleteEntitlement.

All three Bonbon resource types are annotated with OptInRequired{}
since Bonbon is private preview. CapabilityPermissions list the
account-access:* actions the connector calls.

Test scaffold:
  - test/bonbon-testserver/ — in-memory mock for the 11 routes
  - pkg/connector/bonbon/connector_test.go covers ValidateRegion;
    TestBonbonFullSync is t.Skip()'d for the follow-up wiring PR

docs/bonbon.md covers the customer-side onboarding (Org-wide vs
standalone enablement, the required trust policy with sts:AssumeRole
+ sts:SetContext, IAM permissions, and the private-preview
limitations carried over from the AWS onboarding guide).

This is a scaffold PR — the sync wiring is in place and compiles,
but TestBonbonFullSync needs the testserver auth path finalized
before assertions can land. Refs CXH-1558.
@linear-code

linear-code Bot commented May 26, 2026

Copy link
Copy Markdown

CXH-1558

Comment on lines +40 to +65
for _, appArn := range b.applicationArns {
token := ""
for {
in := &client.ListEntitlementsInput{
ApplicationArn: appArn,
Filter: client.EntitlementFilter{},
NextToken: token,
}
out, err := b.client.ListEntitlements(ctx, in)
if err != nil {
return nil, nil, fmt.Errorf("baton-aws/bonbon: ListEntitlements(%s): %w", appArn, err)
}
for _, member := range out.Entitlements {
if member.Entitlement.PrincipalRole == nil {
continue
}
roleArn := member.Entitlement.PrincipalRole.RoleArn
account := member.Entitlement.PrincipalRole.Account
seenRoles[roleArn] = account
}
if out.NextToken == "" {
break
}
token = out.NextToken
}
}

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.

🔴 Bug: F2 violation — List() fetches all entitlement pages internally. This nested loop iterates through every application and every entitlement page to build the seenRoles map, bypassing SDK-driven pagination entirely. The SyncOpAttrs page token parameter is ignored. This means no checkpointing (crash = restart from scratch), no SDK rate-limit feedback, and unbounded memory for accounts with many entitlements.

The dedup requirement (discovering unique role ARNs) makes this harder to paginate than a simple list, but the current approach can OOM on large deployments. Consider using the SDK pagination bag to track progress across applications and pages, yielding discovered roles incrementally (the SDK deduplicates resources by ID).

Comment on lines +112 to +146
for _, appArn := range b.applicationArns {
token := ""
for {
in := &client.ListEntitlementsInput{
ApplicationArn: appArn,
Filter: client.EntitlementFilter{
PrincipalRole: &client.PrincipalRoleEntitlementFilter{RoleArn: roleArn},
},
NextToken: token,
}
page, err := b.client.ListEntitlements(ctx, in)
if err != nil {
return nil, nil, fmt.Errorf("baton-aws/bonbon: ListEntitlements(%s, %s): %w", appArn, roleArn, err)
}
for _, member := range page.Entitlements {
if member.Entitlement.PrincipalRole == nil {
continue
}
if member.Entitlement.PrincipalRole.RoleArn != roleArn {
continue
}
grant, err := principalGrant(resource, &member)
if err != nil {
return nil, nil, err
}
if grant != nil {
out = append(out, grant)
}
}
if page.NextToken == "" {
break
}
token = page.NextToken
}
}

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.

🔴 Bug: F2 violation — Grants() fetches all entitlement pages internally. Same issue as List(): the inner for loop fetches every page of entitlements across all applications in a single Grants() call. The SyncOpAttrs page token is ignored and SyncOpResults is returned as nil, so the SDK treats this as one giant page.

Unlike List(), this is straightforward to fix with SDK pagination bags — use the bag to track which (appArn, pageToken) pair you're on, yield one page of grants per call, and return NextPageToken to let the SDK drive the loop.

Comment on lines +171 to +173
if _, err := b.client.CreateEntitlement(ctx, in); err != nil {
if client.IsCode(err, client.ErrAlreadyCreated) {
return nil, nil

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.

🟡 Suggestion: The AlreadyCreated case is handled correctly (returns nil error), but the standard SDK convention is to return a GrantAlreadyExists annotation so ConductorOne can distinguish "grant was already in place" from "grant was just created." Same applies to the Revoke method below — ResourceNotFound should return a GrantAlreadyRevoked annotation.

Comment on lines +483 to +496
if err != nil {
l.Error("baton-aws/bonbon: failed to resolve calling config", zap.Error(err))
return rs
}
builders, err := bonbon.NewBuilders(ctx, bonbonCfg, bonbon.Options{
Region: c.bonbonRegion,
ApplicationArn: c.bonbonApplicationArn,
BaseURL: c.bonbonBaseURL,
HTTPClient: c.baseClient,
})
if err != nil {
l.Error("baton-aws/bonbon: NewBuilders failed", zap.Error(err))
return rs
}

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.

🟡 Suggestion: Both error paths log and return rs without the Bonbon builders. If Bonbon was previously syncing successfully, the absence of its builders this time could cause the SDK to interpret existing Bonbon resources as deleted (F1/F3 concern). Since DefaultCapabilityBuilders() always declares Bonbon resource types, the safest approach may be to return the nil-client capability builders here (matching what DefaultCapabilityBuilders does) so the resource types stay registered even when the client can't be created, and let the builders themselves error at sync time.

@github-actions

Copy link
Copy Markdown
Contributor

Connector PR Review: feat: initial Bonbon (Account Access) connector scaffold

Blocking Issues: 2 | Suggestions: 2 | Threads Resolved: 0
Review mode: full
View review run

Review Summary

This PR scaffolds a new Bonbon (AWS Account Access) connector with a hand-rolled SigV4 client, three resource types, provisioning (Grant/Revoke), a test server, and config wiring. The client, types, error handling, application builder pagination, and resource type definitions are well-structured. However, the role builder's List() and Grants() methods both fetch all entitlement pages in internal loops, bypassing SDK-driven pagination — this violates F2 and can cause OOM on large deployments.

Security Issues

None found.

Correctness Issues

  • pkg/connector/bonbon/role.go:40-65 — F2: roleBuilder.List() fetches all entitlement pages internally in a nested loop, ignoring the SDK page token. No checkpointing, unbounded memory.
  • pkg/connector/bonbon/role.go:112-146 — F2: roleBuilder.Grants() same issue — fetches all pages in an inner loop, returns nil SyncOpResults. Straightforward to fix with SDK pagination bags.

Suggestions

  • pkg/connector/bonbon/role.go:171-173 — Grant/Revoke handle idempotent cases correctly but don't return GrantAlreadyExists / GrantAlreadyRevoked annotations per SDK convention.
  • pkg/connector/connector.go:483-496 — Error paths silently drop Bonbon builders; if Bonbon was previously syncing, this could cause the SDK to treat existing resources as deleted (F1/F3 concern).
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `pkg/connector/bonbon/role.go`:
- Around lines 30-85: roleBuilder.List() contains a nested for loop (lines 40-65) that iterates
  through all applications and all entitlement pages to build a seenRoles map. The SyncOpAttrs
  page token is ignored. Refactor to use SDK pagination bags: track progress as (appIndex,
  pageToken), yield discovered roles incrementally per page, and return NextPageToken via
  SyncOpResults so the SDK drives the loop. The SDK deduplicates resources by ID, so yielding
  the same role ARN from multiple pages is safe.

- Around lines 101-148: roleBuilder.Grants() contains the same nested pagination loop pattern
  (lines 112-146). Refactor to use the SDK pagination bag to track which (appArn, pageToken)
  pair the method is on, yield one page of grants per call, and return NextPageToken in
  SyncOpResults.

## Suggestions

In `pkg/connector/bonbon/role.go`:
- Around line 172-173: When AlreadyCreated is detected in Grant(), return
  annotations.New(&v2.GrantAlreadyExists{}) instead of bare nil annotations. Similarly at
  line 190-191, when ResourceNotFound is detected in Revoke(), return
  annotations.New(&v2.GrantAlreadyRevoked{}) instead of bare nil.

In `pkg/connector/connector.go`:
- Around lines 483-496: When getCallingConfig or NewBuilders fails for Bonbon, instead of
  returning rs without Bonbon builders, append bonbon.DefaultCapabilityBuilders() (nil-client
  builders) so the resource types stay registered. The builders will error at sync time with a
  clear message rather than silently disappearing.

@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.

Blocking issues found — see review comments.

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