Skip to content

[CXH-2209] fix: gate clm_role on CLM availability, narrow opt-in error tolerance, log skipped folder-security entries - #64

Open
FeliLucero1 wants to merge 37 commits into
clm-folder-provisioning-external-idfrom
clm-role-optin-error-observability-fixes
Open

[CXH-2209] fix: gate clm_role on CLM availability, narrow opt-in error tolerance, log skipped folder-security entries#64
FeliLucero1 wants to merge 37 commits into
clm-folder-provisioning-external-idfrom
clm-role-optin-error-observability-fixes

Conversation

@FeliLucero1

@FeliLucero1 FeliLucero1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Update (2026-08-13)

The plan in bullet 2 below was superseded twice since this was first written — the final shipped behavior is simpler than either version described there:

  • All 6 originally-tolerant resource types now fail loudly instead of gracefully skipping when their feature is unavailable: clm_role, clm_folder, clm_group, clm_member, clm_permission_set, and signing_group (not a CLM type, but gated by the same isOptInFeatureUnavailableError tolerance and the same review objection). Per reviewer feedback (luisina-santos): an opted-in resource that can't actually reach its API is a misconfiguration to surface, not a state to tolerate silently — "cuando no se tienen los permisos o la capacidad suficiente para sincronizar un recurso la sync TIENE que fallar." This applies even though each of clm_role/clm_folder/.../signing_group is registered unconditionally and gated only by OptInRequired (or, for signing_group, the --include-signing-groups flag) — once opted in, a real capability failure now fails the sync rather than returning success with zero resources.
  • The isOptInFeatureUnavailableError/client.IsClmDiscoveryError/clmDiscoveryError machinery bullet 2 describes (the discovery-vs-data-call distinction) is gone entirely — there's no tolerance branch left for it to narrow.
  • Found along the way: OptInRequired is enforced only by ConductorOne's platform (the sync-orchestration layer), never by baton-sdk or this connector itself. Any self-hosted/CLI run of the connector — including this repo's own CI — attempts every resource type unconditionally regardless of opt-in status. This repo's test-groups/test-signing-groups/test-permission-profiles CI jobs broke for exactly this reason once the fail-loud change landed; fixed by pinning BATON_SYNC_RESOURCE_TYPES in .github/workflows/ci.yaml, and added a README/docs note for anyone else running the connector directly.
  • Bullet 3's folder-security logging also went through several more review rounds: removed a dead conjunct in logSkippedFolderSecurityEntry's early return, and fixed a handful of comments left stale by the fail-loud change and by centralizing the per-branch logging into that one helper.
  • Added singing_groups_test.go (previously no test file existed for this builder at all) covering the new fail-loud behavior.

See the commit history for the full sequence — it includes one full revert (c12934e) of an intermediate attempt that reintroduced a regression already ruled out earlier in this same PR, caught by review before merge.


Description

  • Bug fix
  • New feature

Three independent CLM sync bugs, all reproducible without a CLM tenant (found via a hypothesis-investigation pass, 16/16 hypotheses confirmed across the three findings):

  1. clm_role synced 5 fixed resources unconditionally, even on accounts without CLM. List() never made an API call, so it had no way to detect a missing subscription — unlike every other CLM resource type. Now gated on the same account-discovery check (client.EnsureClmReady) the other CLM builders already run internally.

    Behavior change: on an account without CLM that has already synced once under the old (buggy) behavior, the next sync after this ships will show these 5 clm_role resources going from present to absent, which C1 reads as deletions. This is the correct end state (those 5 roles never should have existed for a non-CLM account), and low-blast-radius: clm_role has no entitlements/grants of its own, and its only real use — as a principal in a clm_folder security grant — never applied either, since clm_folder is gated by the same check and never synced any folders/grants on that same non-CLM account. No customer is using CLM today, so this has no real-world impact right now, but flagging it here per review feedback in case that changes before this ships.

  2. isOptInFeatureUnavailableError tolerated 401/403/404/412 from any CLM call, not just the account-discovery call it was reasoned aboutsuperseded, see the Update above. (Original text preserved for history: tried tightening this to require the error come from discovery specifically; reverted after review feedback showed a genuine "no CLM subscription" signal can legitimately come from the data call too depending on where DocuSign enforces the check, and this repo has no live CLM tenant to confirm which. Landed instead as a narrower gate specific to clm_role, then ultimately replaced entirely by the fail-loud change above.)

  3. clm_folder's Grants() silently skipped folder-security entries with an unmapped AccessType (Custom, InheritFromParentFolder, an unrecognized role) with no log output at any level. Added debug-level logging at each skip point.

Stacked on #63.

Useful links:

@FeliLucero1 FeliLucero1 changed the title fix: gate clm_role on CLM availability, narrow opt-in error tolerance, log skipped folder-security entries [CXH-2209] fix: gate clm_role on CLM availability, narrow opt-in error tolerance, log skipped folder-security entries Aug 7, 2026
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

CXH-2209

Comment thread pkg/connector/helper.go Outdated
Comment thread pkg/connector/clm_roles.go Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Connector PR Review: [CXH-2209] fix: gate clm_role on CLM availability, narrow opt-in error tolerance, log skipped folder-security entries

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: none loaded - base ref clm-folder-provisioning-external-id is not the default branch main for .claude/skills/ci-review.md.
Review mode: full
View review run

Review Summary

Scanned the full PR diff for security and correctness. 9dd87cf is a rebase/squash of the previously reviewed tree onto the new base 7487101 (which carries the workflow-level concurrency block); comparing 47900c10 against this SHA file-by-file shows no content change — the corrected signingGroupResourceType comment in resource_types.go:32, the Validate() centralization, the isOptInFeatureUnavailableError removal across all six builders, the logSkippedFolderSecurityEntry helper, and the new singing_groups_test.go / connector_test.go coverage are all identical to the last reviewed tree. No new security or correctness defects, and no new findings this round.

The three prior suggestions were re-verified against the current code and all three are still open at the same lines, so they are carried forward below rather than restated inline. New evidence on the second one: tracing the vendored SDK, every cli.RunTimeOpts construction site — pkg/cli/commands.go:462 (service/daemon mode), :661, :792, and pkg/cli/lambda_server__added.go:494 — populates SyncResourceTypeIDs exclusively from the local sync-resource-types viper key, with no separate channel for a platform-delivered per-tenant opt-in set. That narrows the open question to a single factual one: does ConductorOne set the sync-resource-types config knob on hosted/service-mode deployments? If it does not, WillSyncResourceType returns true for every ID, includeClm is always true, and Validate() aborts the whole sync at syncer.go:1146 for every eSignature-only tenant.

Dependency manifests are unchanged: go.mod / go.sum are untouched, and vendor/modules.txt only gains go.uber.org/zap/zaptest/observer, a package of the already-required go.uber.org/zap module used solely by the new log-assertion tests. The CI BATON_SYNC_RESOURCE_TYPES allowlist was verified empirically again from the test-groups job log on the last completed ci run (Synced resource types, count 4, with list-resources for exactly user, signing_group, group, permission_profile), so the comma-separated env value parses as intended and includeClm resolves to false in CI.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • pkg/connector/connector.go:228 — (carried forward, still open) the opts == nil || guard is dead code: New() dereferences opts unconditionally at lines 239 and 244, so nil opts panics regardless.
  • pkg/connector/connector.go:155 — (carried forward, still open) includeClm hard-gates the whole sync on the platform delivering a sync-resource-types filter; confirmed above that the only channel the SDK has for that filter is the local config knob.
  • pkg/connector/singing_groups.go:41 — (carried forward, still open) signing groups get the fail-loud change but not the upfront Validate() check, so they fail mid-sync while CLM fails upfront; docs/connector.mdx presents both identically.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/connector.go`:
- Around lines 224-230: The `includeClm := opts == nil || ...` short-circuit and its
  four-line explanatory comment can never take effect, because `New()` dereferences
  `opts` unconditionally at line 239 (`opts.SelectedAuthMethod`) and line 244
  (`opts.TokenSource`). The pre-existing `opts != nil` conjunct on line 242 is dead for
  the same reason. Pick one: either add a single real guard at the top of `New()` that
  substitutes an empty ConnectorOpts value when opts is nil, and delete the
  per-expression nil checks, or delete the nil handling and the comment entirely. As
  written the code documents a nil-safety contract it does not honor.
- Around line 155: `Validate()` gates the CLM readiness check on `includeClm`, which is
  derived from `opts.WillSyncResourceType(...)`. That helper returns true whenever
  `SyncResourceTypeIDs` is empty. Tracing the vendored SDK, all four `cli.RunTimeOpts`
  construction sites (pkg/cli/commands.go:462, :661, :792 and
  pkg/cli/lambda_server__added.go:494) fill `SyncResourceTypeIDs` only from the local
  `sync-resource-types` viper key -- there is no separate platform channel carrying the
  customer OptInRequired selections. Confirm with the ConductorOne platform team that
  hosted/service-mode deployments actually set that config knob. If they do not, every
  eSignature-only customer gets includeClm=true and `Validate()` aborts the entire sync
  (users, groups and permission profiles included) at baton-sdk syncer.go:1146 rather
  than merely skipping CLM. If the filter is not guaranteed, gate the CLM check on an
  explicit config flag instead of on the sync filter.

In `pkg/connector/singing_groups.go`:
- Around line 41: `List()` now propagates the feature-unavailable error, but nothing
  checks signing-group availability in `Connector.Validate()`, unlike CLM. An operator
  who sets --include-signing-groups on an account without the feature therefore fails
  mid-sync, after `user` / `group` / `permission_profile` have already partially synced,
  while the CLM equivalent fails upfront. `docs/connector.mdx` describes both cases
  identically. Either add a signing-groups probe to `Validate()` gated on
  `includeSigningGroups` (mirroring the `includeClm` gate), or add a doc comment here
  explaining why signing groups deliberately remain a mid-sync failure.

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

@FeliLucero1
FeliLucero1 force-pushed the clm-role-optin-error-observability-fixes branch from 7877d6a to 11ef697 Compare August 7, 2026 17:18

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

@FeliLucero1
FeliLucero1 force-pushed the clm-role-optin-error-observability-fixes branch from 11ef697 to 9a33a95 Compare August 7, 2026 17:55
Comment thread pkg/connector/helper.go
Comment thread pkg/connector/helper.go Outdated

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

Comment thread pkg/connector/helper_test.go Outdated
Comment thread pkg/connector/helper.go Outdated

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

@FeliLucero1
FeliLucero1 force-pushed the clm-role-optin-error-observability-fixes branch from c400c19 to c003e08 Compare August 10, 2026 15:40
Comment thread pkg/connector/clm_roles.go Outdated
Comment thread pkg/connector/clm_roles_test.go Outdated

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

FeliLucero1 added a commit that referenced this pull request Aug 10, 2026
- TestClmSkipLogLevel now asserts the actually-emitted log level via
  zaptest/observer instead of comparing method-value pointers with
  reflect — Pointer() on a func is documented as not guaranteed to
  uniquely identify a function, so the old assertion tested identity
  that happened to work, not the observable behavior the test cares
  about.
- Trimmed clmSkipLogLevel's doc comment: it was narrating this PR's own
  review history ("only two tries at that were made and both were
  wrong", referencing a since-removed IsClmDiscoveryError gate) instead
  of standing on its own — a future reader can't check a claim about
  code that no longer exists, and the paragraph would only rot further.

Other bot comments on this PR (helper.go's "requiring IsClmDiscoveryError
removes graceful degradation" x2, a double-blank-line formatting note)
are stale: they describe the isClmUnavailableError gating mechanism this
PR already reverted away from in a prior commit, and the blank line was
already fixed there too. GitHub re-anchored their commit references to
HEAD after the stack rebase, but their content predates the revert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FeliLucero1
FeliLucero1 force-pushed the clm-role-optin-error-observability-fixes branch from c003e08 to 01763a1 Compare August 10, 2026 16:17
Comment thread pkg/connector/clm_folders.go Outdated

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

FeliLucero1 added a commit that referenced this pull request Aug 10, 2026
- TestClmSkipLogLevel now asserts the actually-emitted log level via
  zaptest/observer instead of comparing method-value pointers with
  reflect — Pointer() on a func is documented as not guaranteed to
  uniquely identify a function, so the old assertion tested identity
  that happened to work, not the observable behavior the test cares
  about.
- Trimmed clmSkipLogLevel's doc comment: it was narrating this PR's own
  review history ("only two tries at that were made and both were
  wrong", referencing a since-removed IsClmDiscoveryError gate) instead
  of standing on its own — a future reader can't check a claim about
  code that no longer exists, and the paragraph would only rot further.

Other bot comments on this PR (helper.go's "requiring IsClmDiscoveryError
removes graceful degradation" x2, a double-blank-line formatting note)
are stale: they describe the isClmUnavailableError gating mechanism this
PR already reverted away from in a prior commit, and the blank line was
already fixed there too. GitHub re-anchored their commit references to
HEAD after the stack rebase, but their content predates the revert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FeliLucero1
FeliLucero1 force-pushed the clm-role-optin-error-observability-fixes branch from 01763a1 to e5a2f17 Compare August 10, 2026 17:02
Comment thread pkg/connector/clm_roles.go Outdated
Comment thread pkg/connector/clm_folders.go Outdated

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

FeliLucero1 added a commit that referenced this pull request Aug 10, 2026
- TestClmSkipLogLevel now asserts the actually-emitted log level via
  zaptest/observer instead of comparing method-value pointers with
  reflect — Pointer() on a func is documented as not guaranteed to
  uniquely identify a function, so the old assertion tested identity
  that happened to work, not the observable behavior the test cares
  about.
- Trimmed clmSkipLogLevel's doc comment: it was narrating this PR's own
  review history ("only two tries at that were made and both were
  wrong", referencing a since-removed IsClmDiscoveryError gate) instead
  of standing on its own — a future reader can't check a claim about
  code that no longer exists, and the paragraph would only rot further.

Other bot comments on this PR (helper.go's "requiring IsClmDiscoveryError
removes graceful degradation" x2, a double-blank-line formatting note)
are stale: they describe the isClmUnavailableError gating mechanism this
PR already reverted away from in a prior commit, and the blank line was
already fixed there too. GitHub re-anchored their commit references to
HEAD after the stack rebase, but their content predates the revert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FeliLucero1
FeliLucero1 force-pushed the clm-role-optin-error-observability-fixes branch from e5a2f17 to 44031de Compare August 10, 2026 18:51
FeliLucero1 and others added 29 commits August 18, 2026 01:43
…ty gate

TestClmRoleBuilder_List_SkipsGracefullyWhenClmUnavailable's bad-token
client happens to fail at CLM discovery itself in this mock (eSignature's
/oauth/userinfo has no auth check), so it only ever pinned the
isOptInFeatureUnavailableError conjunct — deleting
"client.IsClmDiscoveryError(err) &&" from the gate left the whole suite
green. Adds Server.ForceUserInfoStatus to clmtest (mirroring
ForceClmDiscoveryStatus) to force a tolerated code out of
ensureInitialized specifically, and a test asserting List() still fails
loud for it. Verified the new test fails against the exact mutation
before restoring the real code.
clmSkipLogLevel logged the "CLM unavailable, skipping sync" line at Warn
whenever the tolerated error wasn't from CLM account discovery itself.
This repo can't confirm where DocuSign actually enforces the "no CLM
subscription" check — if it turns out to be at the per-resource data call
for most eSignature-only accounts (the common case), the Warn branch
becomes the STEADY STATE for the majority of syncs, inverting what Warn is
supposed to signal (the noisy case ends up being the expected one).

Replaces it with clmDiscoverySourceField: always log at Info, but attach
a from_clm_discovery bool field so a dashboard/alert can still key on the
source without either log-level assumption backfiring depending on which
turns out to be the common case.
zap.Error(err) already carries the full wrapped error text, and every
CLM client method names its own operation in that text ("failed to
discover the CLM API base URL" vs "failed to search CLM folders" vs
"failed to list CLM groups", etc.) — so a human or a Datadog search
reading the log line already knows exactly what failed and where, without
needing a separate field to encode "did this come from discovery."
The field was solving a problem the existing zap.Error(err) already
solved. All 5 CLM builders now log a plain Info line with just the error.
…ceClmDiscoveryStatus doc

clmIsBenignUnmappedAccessType is re-checked by hand in each of the
Groups/Roles/Users loops in clm_folders.go's Grants(), so a copy-paste
slip in just one of them left a Groups-only test green. Seeds all three
collections and scopes the log assertion to the access_type field key so
it isn't brittle to unrelated log traffic from the HTTP/cache layer.

Also fixes ForceClmDiscoveryStatus's doc, which overclaimed "every
subsequent call" when handleClmAccountDiscovery is registered behind
requireAuth — only a client presenting the fixed test bearer token
actually reaches the forced status.
…ccess_type

Seeding all three collections catches an omitted/inverted guard, but the
three log entries were treated as interchangeable — a copy-paste slip
that fires the right branch's guard under another branch's field name
(e.g. the Users loop logging group_href instead of member_href) still
produced 3 Debug entries with access_type: SomethingUnrecognized and
passed. Now asserts each of group_href/role/member_href appears exactly
once across the three entries.
- Give Custom its own distinct Debug log in Grants() (Groups/Roles/Users
  branches): it's a real, active grant this connector can't round-trip to
  a single tier, unlike NoAccess/InheritFromParentFolder, which stay fully
  silent since they're expected inert states, not a visibility gap.
- Pin TestClmRoleBuilder_List_FailsLoudlyOnNonDiscoveryTolerableError's
  preconditions (isOptInFeatureUnavailableError + !IsClmDiscoveryError) so
  it stays a mutation-killer for the IsClmDiscoveryError conjunct even if
  the userinfo failure's code mapping changes later.
- Note clm_role's residual gap: its gate only helps when DocuSign rejects
  CLM at discovery, not on a later per-resource data call.
Catches a branch swapping its distinguishing field/message with another
(e.g. Users logging group_href) without exercising every combination —
the previous check only verified each field appeared somewhere across
the 3 entries. Also renames the shadowing ctx local to fields.
Groups/Roles/Users each had a near-verbatim 8-line switch differing only
in the noun and one field name — collapsed into logSkippedFolderSecurityEntry.
Also fixes the Custom branch missing the access_type field that every other
skip line carries, and rescopes its test to the same access_type-filtered
query as its sibling test instead of unscoped logs.All().
…mple-branch Grant test

The unrecognized-role skip line was the one skip line in clm_folders.go
without an access_type field, contradicting logSkippedFolderSecurityEntry's
own doc claim about a single consistent structured-log query.

TestClmFolderBuilder_Grant_SurvivesIdentityOnlyPrincipal_SampleBranch had
the same byte-identical-fallback gap just fixed on the group builder's
equivalent test: srv.GroupHref/MemberHref build the same shape the
fallback derivation would from the same discovered base URL, so the test
passed even with clmPreferredHref's sample-preference loop deleted
(verified via mutation test). Re-seeds folder-contracts' samples with an
alternate host to make the two branches actually distinguishable.
…r-security skip

fmt.Sprintf and the fields append() ran unconditionally before zap's Debug-level
check, on every skipped entry, every sync (confirmed via hypothesis investigation,
5/5). Switches to one constant message per case plus a principal_kind field,
removing the Sprintf cost entirely. Updates the test that keyed on the old
per-kind message text to bind principal_kind -> distinguishing field instead.
- unrecognized-role skip line still had no principal_kind field, unlike
  every other skip line that field exists to make queryable.
- logSkippedFolderSecurityEntry's fields append() ran before deciding
  whether to log at all, so the common benign case (NoAccess/Inherit, on
  every folder of every sync) still allocated for a line never written.
  Early-return before the append, same spirit as the Sprintf removal.
PR #63 and this branch independently fixed the same tautological-test
finding on TestClmFolderBuilder_Grant_SurvivesIdentityOnlyPrincipal_SampleBranch
(a byte-identical sample-derived vs. fallback-derived Href) via two
different mechanisms — Server.SetFolderGroupSecurityHref/
SetFolderUserSecurityHref (surgical Href override) vs. a full
PatchFolderSecurity re-seed — which the rebase concatenated into one
function body (duplicate sampleHost/wantHref declarations, and the
re-seed silently overriding the override). Keep the surgical version:
it preserves folder-contracts' original seed shape (both group
entries) instead of replacing it.
…builders

clm_roles.go already requires client.IsClmDiscoveryError(err) in addition
to isOptInFeatureUnavailableError(err) before soft-skipping a sync, since
the code alone can come from either CLM account discovery or a later
per-resource data call. clm_folders/clm_groups/clm_members/clm_permission_sets
each call ensureClmReady first (via SearchFolders/ListGroups/ListMembers/
ListPermissionSets), whose own data-call failures are never wrapped as
clmDiscoveryError, so the same conjunct applies cleanly here too — closing
the gap flagged in PR review (CXH-2209).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…M builders

e85ccf9 re-applied exactly the narrowing eca81a7 deliberately reverted
earlier in this PR, for the same reason that revert gave: this repo has
no live CLM tenant to confirm whether DocuSign rejects a non-CLM account
at discovery or at the per-resource data call. Unlike clm_role (which has
no data call of its own, so IsClmDiscoveryError can only ever reject a
non-discovery failure that already breaks every other resource type too),
clm_folder/clm_group/clm_member/clm_permission_set each make a real data
call after discovery succeeds. Requiring IsClmDiscoveryError there turns
a same-coded rejection from that data call — e.g. a token that loses scope
mid-sync — into a hard List() error, which baton-sdk's parallel syncer
treats as fatal for the ENTIRE sync (all resource types, not just the one
CLM builder), not a per-resource-type skip. That's a worse regression than
the narrower gap being closed. Restores isOptInFeatureUnavailableError(err)
alone for these four builders; clm_role's own two-conjunct gate is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…is unavailable

Every resource type that used isOptInFeatureUnavailableError to tolerate
"feature not available" errors (clm_role, clm_folder, clm_group, clm_member,
clm_permission_set, signing_group) carries OptInRequired: C1 excludes them
from a customer's sync by default, and List() only ever runs once a customer
has explicitly opted in. C1's opt-in toggle has no upstream check against
DocuSign, so a customer can enable one of these without actually having the
subscription/scopes it needs.

Per review feedback (luisina-santos): that's a real misconfiguration, not an
expected/transient state, and every connector should fail its sync when it
lacks sufficient permission or capability to sync an opted-in resource -
silently succeeding with zero resources just hides the problem instead of
surfacing it. This also fully closes the discovery-vs-data-call ambiguity
debated earlier in this PR (mateoHernandez123's finding, the bot's regression
finding) - there's no longer a tolerance branch left to gate.

Removes the now-dead isOptInFeatureUnavailableError helper and its test.
client.IsClmDiscoveryError is left in pkg/client (still tested there, still
documents a real distinction in the client's error taxonomy) even though it
has no remaining connector-layer caller - deleting it is a separate,
client-layer cleanup, not part of this behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…types

test-groups/test-signing-groups/test-permission-profiles run baton-docusign
directly via the sync-test action, with no C1 platform in the loop and no
--sync-resource-types filter — so every registered resource type, including
the 5 OptInRequired clm_* types, gets attempted regardless of whether anyone
"opted in." Since 47f58c3 made CLM builders fail loud instead of skipping
gracefully, and this repo's CI DocuSign account has never had a CLM
subscription, every one of these jobs now fails the instant clm_folder's
List() hits the CLM discovery 401 - confirmed by diffing this exact job's
log against the immediately preceding (passing) commit.

Pin BATON_SYNC_RESOURCE_TYPES to the 4 types each job actually needs
(user, group, permission_profile, signing_group), matching how a real
C1-hosted sync would behave for an account that never opted a CLM type in -
that filtering only happens on C1's side, not in baton-sdk itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…docs gap

Three cleanups surfaced by automated review on 47f58c3:

- clm_client.go's clmDiscoveryError type and exported IsClmDiscoveryError had
  zero remaining callers once isOptInFeatureUnavailableError was deleted (the
  only consumer their own doc comments named never existed under that name).
  ensureClmInitialized now returns its errors unwrapped; removed the type,
  the predicate, and their now-dead unit test.
- clmtest/server.go's ForceClmDiscoveryStatus/ForceUserInfoStatus test knobs
  were purpose-built for the two clm_role regression tests 47f58c3 deleted
  (the two-conjunct gate's discovery-vs-non-discovery halves) and had no
  other caller left.
- docs/connector.mdx's signing-groups paragraph still described the old
  graceful-skip behavior; only the CLM paragraph got updated in 47f58c3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ging

Three more findings from repeated automated review passes on the same area:

- logSkippedFolderSecurityEntry's early return checked
  `accessType != client.ClmAccessTypeCustom && clmIsBenignUnmappedAccessType(accessType)`,
  but clmIsBenignUnmappedAccessType never returns true for Custom (its own
  doc says so), so the left conjunct was never the deciding factor. Also
  corrected the comment's inaccurate "allocation-free" claim (the caller
  builds the variadic fields before this function is ever entered) and
  dropped a dangling "like the Sprintf removal above" with no referent.
- clmIsBenignUnmappedAccessType's doc pointed at "its own Debug log at each
  call site" for Custom, which was true before logSkippedFolderSecurityEntry
  centralized that logging into one shared function.
- A test comment claimed clmIsBenignUnmappedAccessType was "re-checked by
  hand in each of the Groups/Roles/Users loops" — also stale post-dedupe;
  retargeted to the actual reason for seeding all three collections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
47f58c3's fail-loud behavior change was documented purely in terms of C1's
opt-in toggle, but that gate is platform-side only (confirmed earlier this
PR, when the CI jobs needed BATON_SYNC_RESOURCE_TYPES added to keep
passing). Running baton-docusign directly — the brew/docker/go install
quickstarts, or any self-hosted/CLI invocation — attempts all 5 CLM
resource types unconditionally, so an eSignature-only account run this way
now fails its entire sync instead of skipping CLM. Added a paragraph
pointing at --sync-resource-types/BATON_SYNC_RESOURCE_TYPES so the
workaround isn't discoverable only by reading ci.yaml.

Also fixed one more stale "at each Grants() call site" reference in
clm_folders_test.go — same claim as clm_folders.go:454, missed there in
4d7ed4d's fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
signing_groups.go had zero test coverage before this — the fail-loud change
in 47f58c3 (dropping isOptInFeatureUnavailableError's tolerance) was
unverified. This package had no shared eSignature mock server (unlike
pkg/client/clmtest for CLM), so added a small local one covering just what
List() needs: /oauth/userinfo (the failure path GetSigningGroups hits first)
and a bare /signing_groups response (the happy path, to distinguish "the
mock is wired correctly" from "everything errors regardless").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- singing_groups_test.go: fix stale doc comment/failure message about
  the mock not serving /signing_groups (it does, with an empty body);
  seed one signing group instead so the happy-path test also exercises
  parseIntoSigningGroupResource rather than only asserting zero results.
- Drop singing_groups_test.go's duplicate rewriteTransport declaration
  (users_test.go already declares it in the same package) — this was
  a pre-existing `go test ./pkg/connector/...` build failure, caught
  while fixing the above.
- docs/doc-info.md: update the CLM opt-in bullet to match the
  OptInRequired/fail-loud contract README.md and docs/connector.mdx
  already describe, instead of the old no-opt-in-flag wording.
- connector_test.go: update alwaysRegisteredTypeIDs' comment to stop
  crediting gating to isOptInFeatureUnavailableError, which this PR
  deletes from helper.go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BATON_SYNC_RESOURCE_TYPES pins an explicit allowlist (not a CLM-only
exclusion) in all three sync-test jobs. A new non-CLM resource type
registered in connector.go's ResourceSyncers() with no corresponding
update here would get zero CI sync-test coverage, with nothing failing
to signal the gap. Adds a maintainer reminder to the canonical comment
block the other two jobs already point back to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test-groups/test-signing-groups/test-permission-profiles were only
chained via needs within a single workflow run — two different
branches' runs (e.g. this stack's two PRs, pushed close together)
could still race concurrently against the same live demo account,
one run's mid-cycle Grant/Revoke corrupting another's "should be zero
grants after Revoke" assertion. Confirmed via CI history: test-groups
flips pass/fail across commits that never touch pkg/connector/groups.go,
correlated with near-simultaneous run start times across branches.
Adds a shared concurrency group (not ref-scoped) across all three jobs
so only one runs against the real account at a time, queuing the rest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rebasing this branch onto PR #63's latest tip silently reapplied an
older PR #64 commit's import-block patch over helper.go's newly-added
codes/status imports (no conflict was flagged since the patch context
didn't include them), breaking the build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test-groups/test-signing-groups/test-permission-profiles each carried
an identical BATON_SYNC_RESOURCE_TYPES line plus an explanatory
comment, with a note instructing maintainers to keep all three copies
in sync. Declaring it once at the workflow level removes that
drift hazard — all three jobs inherit it the same way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per luisina-santos's review: each opted-in CLM builder independently
checked CLM availability inside its own List(), redundant and racy
under baton-sdk's concurrent resource-type syncing, when a single
upfront check in Validate() (called once, before any List()) produces
the same fail-loud outcome sooner. Centralizes that check behind a new
includeClm field.

Fixed two constructor bugs found while wiring includeClm through:
NewWithTokenSource -- the ConductorOne-hosted path, i.e. the common
production case -- had no includeClm parameter at all, so Validate()
would have silently never run its CLM check there; NewWithRefreshToken
received the parameter but never stored it. Also extended Validate()
to check base eSignature credentials unconditionally (previously an
unconditional no-op for any account without CLM), catching a
misconfigured account upfront instead of leaving that to whichever
builder's List() happens to run first mid-sync.

clm_roles.go's own now-fully-redundant EnsureClmReady call is removed.
NewWithClient (zero callers anywhere in this repo) is kept rather than
deleted, since removing an exported function is a breaking change for
any external consumer that may exist -- documented as such, with a
test matching its two siblings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A self-hosted connector running in service mode still receives the
platform's resource-type filter (confirmed in the c1 repo's
rpc_baton.go SyncResourceTypeIds passthrough) -- only a bare one-shot
CLI invocation, with no service/task involved at all, attempts every
CLM resource type unconditionally. The prior wording lumped both cases
together as "self-hosted/CLI."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A per-job concurrency block only protects a *running* job from
cancellation -- GitHub Actions still cancels a *pending* job in the
same group when a newer one queues. With test-groups/
test-signing-groups/test-permission-profiles needs-chained but each
carrying its own copy of the same group, two overlapping workflow runs
could still cancel each other's pending jobs mid-chain. Declaring it
once at the workflow level instead makes the whole three-job run
queue/cancel as one unit against the shared demo-account group.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The comment claimed signing_group is "registered unconditionally ...
OptInRequired is the gate, not a config flag, matching the CLM types
below" -- but ResourceSyncers() only appends its builder when
includeSigningGroups is set, contradicting the comment,
TestResourceSyncers_SigningGroupRegistrationFollowsFlag's own doc, and
singing_groups_test.go. Caught by the automated PR reviewer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.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.

7 participants