feat(cloudidentity): add set_members enrichment for authoritative group membership (#1313) #136
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/cloudidentity-set-members-1313"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
set_membersenrichment method to the GCP cloudidentitygroups-membershipsmodel that reconciles membership to a supplied desired set — adds missing members, deletes members whosepreferredMemberKey.idis not in the setadd_iam_binding/remove_iam_binding): new.enrich.tssource, metadata registration, codegen regenerationcodegen/designs/gcp.mdsection 18 to document the new enrichmentCloses #1313
Test plan
deno task generate:gcp cloudidentity—set_membersmethod appears in generated outputdeno checkon generated model passesdeno linton generated model passesdeno install --frozenpasses (lockfile integrity)2026.07.24.2with upgrade stanzas🤖 Generated with Claude Code
Code Review
Blocking Issues
None.
Suggestions
Empty-string member IDs pass validation (
cloudidentity-groups-memberships.enrich.ts, line 15):z.string()accepts"". An empty-stringidwould be added todesiredIdsand silently corrupt reconciliation — members with a missingpreferredMemberKey.idwould never match it (falsy guard on line 71 returns false), so they'd be excluded fromtoDelete, making the reconciliation non-authoritative for those edge members. Considerz.string().min(1)(or.email()) on theidfield.Silently skipped members with missing
preferredMemberKey.id(cloudidentity-groups-memberships.enrich.ts, lines 70–73): ThetoDeletefilter drops any current member whosepreferredMemberKey.idis absent (return id && !desiredIds.has(id)). If the API ever returns a member without apreferredMemberKey.id(broken/partial response), that member is silently excluded from deletion. The reconciliation would then not be fully authoritative. This is likely an impossible API state, but a defensive comment or log noting the skip would aid future debugging.No guard for empty
parent(cloudidentity-groups-memberships.enrich.ts, line 38):const parent = String(g["parent"] ?? "")silently allows an empty string. The subsequentlistResourcescall will produce a confusing GCP API error (e.g.,INVALID_ARGUMENT: Invalid parent path) rather than a helpful message. A simple early check (if (!parent) throw new Error("parent is required")) would surface misconfiguration immediately.Design doc authentication note not updated (
codegen/designs/gcp.md, §8 Enrichment limitation): The existing limitation note calls outserviceaccountsandstorage-bucketsas enrichments that callrequest()without forwarding explicit credentials. The newcloudidentity-groups-membershipsenrichment does properly threadcredentialsthrough all API calls (listResources,createResource,deleteResource). This is strictly better than the existing enrichments; noting this in the doc (or updating the limitation note to reflect the pattern has been fixed in newer enrichments) would help future enrichment authors follow the newer approach.Adversarial Review
Medium
Partial failure during delete phase leaves group in unrecoverable intermediate state with no diagnostic context —
cloudidentity-groups-memberships.enrich.ts:110-121The create loop (lines 76-108) runs first and commits members to the group. Then the delete loop (lines 111-121) runs sequentially with no try/catch. If
deleteResourcethrows on the Nth deletion (e.g., transient 503, permission denied on a specific member type, or quota exhaustion), the operation exits with an error, but:created/deletedcounts in the return value are never reached).Concrete scenario: Desired
[A, B, C], current[B, D, E, F]. Creates[A, C]succeed. DeleteDsucceeds. DeleteEfails (transient). Group is now[A, B, C, E, F]— neither original nor desired. The caller gets an opaque error with no indication thatAandCwere added andDwas removed.This is consistent with how other enrichments handle errors (fail-forward, no rollback), but is more consequential here because the method is described as "authoritative reconciliation." A retry with the same inputs will converge (creates are idempotent via AlreadyExistsError handling, remaining deletes will be retried), but only if the caller knows to retry.
Suggested improvement: Wrap the delete loop in a try/catch that captures partial progress and includes it in the thrown error, e.g.:
No concurrency control on the reconciliation window —
cloudidentity-groups-memberships.enrich.ts:46-121The existing IAM enrichments (storage-buckets, serviceaccounts) use etag-based optimistic concurrency on their read-modify-write operations. This enrichment performs a list (line 46), then creates and deletes against the live group with no mechanism to detect concurrent modifications.
Concrete scenario: Between the initial
listResources(line 46) and the delete loop (line 111), another process adds memberXto the group.Xis not indesiredIds(it wasn't in the initial list, so it wasn't considered). The delete loop picks upX... actually no — the delete loop operates ontoDeletewhich was computed from the initial list, soXwouldn't be intoDelete. ButXwould appear in the final reconciled list and be written as state, even though it's not indesiredMembers. The result claimstotal: NincludingX, making the reported reconciliation inaccurate.Conversely, if another process removes a member between the list and the delete loop,
deleteResourcewill fail on a 404 for that member, causing the operation to throw mid-delete (see finding #1).The Cloud Identity Memberships API doesn't support etags on individual memberships, so there may not be a clean fix. But this is a limitation worth documenting, at minimum.
Low
Duplicate IDs in
desiredMemberscause redundant API round-trips —cloudidentity-groups-memberships.enrich.ts:62,69desiredIdsis built as aSet(deduplicating), buttoCreateis filtered from the originaldesiredMembersarray. IfdesiredMemberscontains[{id: "a@x.com"}, {id: "a@x.com"}],toCreatewill include both entries. The second create hitsAlreadyExistsError, triggers a full group re-list (line 95-100), finds the member, and recovers — so it's not a crash, but it's an unnecessary API call. The Zod schema does not enforce uniqueness.Current members without
preferredMemberKey.idare invisible to reconciliation —cloudidentity-groups-memberships.enrich.ts:64-73Members returned by the API without a
preferredMemberKey.idare silently excluded from bothcurrentById(line 65-66) andtoDelete(line 71). These members cannot be managed by this method — they won't be deleted even when absent fromdesiredMembers. This is a defensible choice but means "authoritative" reconciliation is only authoritative over members that have apreferredMemberKey.id. If such members exist, the result'stotalcount will include them, potentially confusing the caller.Final list after reconciliation doesn't check for truncation —
cloudidentity-groups-memberships.enrich.ts:123-129The initial list correctly checks
nextPageTokenand throws if the group is too large (line 55). The final list after reconciliation does not checknextPageToken. If the group grew between the initial and final lists (e.g., concurrent additions),writeResourcehandles would be written for only a subset of members, andresult.totalwould undercount. Unlikely given the initial check passed, but asymmetric validation.Hardcoded
maxPages: 100in AlreadyExistsError fallback path —cloudidentity-groups-memberships.enrich.ts:99-100When a create fails with AlreadyExistsError, the fallback re-lists the group with
maxPageshardcoded to100instead of using the caller'smaxPagesargument. Not a practical bug (if the initial list with the user's maxPages succeeded, 100 pages is almost certainly sufficient), but inconsistent.Verdict
PASS — The enrichment follows existing patterns, handles the primary edge case (AlreadyExistsError race), and the Zod schema is well-defined. The partial-failure and concurrency findings are MEDIUM but consistent with how other enrichments in this codebase work — they don't block merge. The metadata file, index registration, and design doc updates are all correct.