Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b339b0d
fix: gate clm_role on CLM availability, narrow opt-in error tolerance…
FeliLucero1 Aug 7, 2026
1863069
fix: revert opt-in-error narrowing, log instead of gate on error source
FeliLucero1 Aug 7, 2026
b002b0b
fix: address current bot review findings on PR #64
FeliLucero1 Aug 10, 2026
091f923
fix: suppress expected-state Debug logs diluting the AccessType skip …
FeliLucero1 Aug 10, 2026
6b4a8ed
fix: narrow clm_role's CLM-availability tolerance to discovery errors…
FeliLucero1 Aug 10, 2026
d0661b6
fix: require isOptInFeatureUnavailableError alongside IsClmDiscoveryE…
FeliLucero1 Aug 10, 2026
aec3e95
fix: clmIsBenignUnmappedAccessType doc/test correctness (no log-level…
FeliLucero1 Aug 10, 2026
fcf849b
fix: pin which entry logs in the AccessType-skip test, not just how many
FeliLucero1 Aug 10, 2026
96aaebf
test: cover the other half of clm_role's two-condition CLM-availabili…
FeliLucero1 Aug 10, 2026
3ac142e
fix: carry CLM-discovery source as a log field, not a log level
FeliLucero1 Aug 10, 2026
b446fe7
fix: drop clmDiscoverySourceField — the error text already says why
FeliLucero1 Aug 10, 2026
6ff1642
test: cover Roles/Users branches for the AccessType-skip log; fix For…
FeliLucero1 Aug 11, 2026
7ef387f
test: assert each branch's distinguishing log field, not just count/a…
FeliLucero1 Aug 11, 2026
7c97d95
fix: address remaining PR #64 review threads
FeliLucero1 Aug 11, 2026
39312ee
test: bind unmapped-AccessType log assertions per-message, not set-cover
FeliLucero1 Aug 11, 2026
241ad3d
refactor: dedupe folder-security skip logging into one helper
FeliLucero1 Aug 11, 2026
b48a9d8
fix: add missing access_type to unrecognized-role skip log; harden sa…
FeliLucero1 Aug 11, 2026
fa73fbc
perf: stop building a per-kind message via fmt.Sprintf on every folde…
FeliLucero1 Aug 11, 2026
03901dc
fix: close two gaps left by the previous skip-log pass
FeliLucero1 Aug 11, 2026
6bbf8a0
fix: resolve duplicate sample-branch re-seed logic from rebase conflict
FeliLucero1 Aug 12, 2026
31aae8a
fix: apply the discovery-source two-conjunct gate to the other 4 CLM …
FeliLucero1 Aug 12, 2026
e988e24
revert: pull back the discovery-source gate from the 4 data-backed CL…
FeliLucero1 Aug 12, 2026
940dd66
fix: fail loudly instead of silently skipping when an opt-in feature …
FeliLucero1 Aug 12, 2026
a69026f
fix: exclude CLM/signing-group risk from CI by pinning sync-resource-…
FeliLucero1 Aug 13, 2026
01b398f
refactor: drop dead CLM-discovery-error machinery, fix signing_group …
FeliLucero1 Aug 13, 2026
de6d113
fix: drop dead conjunct and fix stale comments in folder-security log…
FeliLucero1 Aug 13, 2026
4c11459
docs: warn self-hosted/CLI users that OptInRequired doesn't protect them
FeliLucero1 Aug 13, 2026
151555c
test: add signing_groups_test.go, covering the new fail-loud behavior
FeliLucero1 Aug 13, 2026
72d9199
fix: address bot review findings on PR #64, dedupe rewriteTransport
FeliLucero1 Aug 13, 2026
15d592c
docs: warn maintainers that CI's resource-type allowlist needs updating
FeliLucero1 Aug 13, 2026
efba5fb
fix: serialize CI jobs that hit the shared DocuSign demo account
FeliLucero1 Aug 13, 2026
f56957e
docs: dedupe BATON_SYNC_RESOURCE_TYPES into a workflow-level env block
FeliLucero1 Aug 13, 2026
760b67e
fix: centralize CLM and base-credential checks in Connector.Validate()
FeliLucero1 Aug 18, 2026
60751be
docs: narrow README's self-hosted/CLI language to the one-shot CLI case
FeliLucero1 Aug 18, 2026
09573b2
fix: hoist ci.yaml's per-job concurrency block to workflow level
FeliLucero1 Aug 18, 2026
49e2e6c
fix: correct stale signingGroupResourceType registration comment
FeliLucero1 Aug 18, 2026
e2889b7
fix: drop NewWithClient's self-contradictory compatibility rationale
FeliLucero1 Aug 18, 2026
8cc9abf
fix: delete dead NewWithClient constructor and its test
FeliLucero1 Aug 18, 2026
f31ec4c
fix: wrap Validate() errors with context, trim design-log comments
FeliLucero1 Aug 20, 2026
668a17b
fix: wrap connector.go:158's error string to satisfy revive line-leng…
FeliLucero1 Aug 20, 2026
dac1f92
fix: drop dead opts==nil guard, fix doc-info.md CLM opt-in contradiction
FeliLucero1 Aug 20, 2026
d2f3a3e
docs: trim folder-security helper comments per review
FeliLucero1 Aug 21, 2026
85a5fdd
docs: record the includeClm/Validate() gap as a reviewed, accepted tr…
FeliLucero1 Aug 24, 2026
5edc9ee
test: cover the actual removed tolerance in signing_groups' fail-loud…
FeliLucero1 Aug 24, 2026
9ecc8d6
docs: drop the impersonation scope claim, confirmed reverted from the…
FeliLucero1 Aug 24, 2026
1b93916
docs: note ci.yaml's signing_group allowlist entry needs its per-job …
FeliLucero1 Aug 24, 2026
023b3c2
fix: drop clmRoleBuilder's dead client field
FeliLucero1 Aug 24, 2026
c7d5496
docs: correct scope claim in the Validate()/includeClm decision record
FeliLucero1 Aug 24, 2026
16a9ef1
test: pin New()'s includeClm derivation from opts.SyncResourceTypeIDs
FeliLucero1 Aug 24, 2026
8bd0005
fix: derive TestNew_IncludeClmDerivation's non-CLM allowlist from alw…
FeliLucero1 Aug 24, 2026
15264e5
fix: enforce nonClmAllowlist() against ci.yaml, not just against itself
FeliLucero1 Aug 24, 2026
ad17858
fix: scope TestNonClmAllowlistMatchesCI's failure to the key it actua…
FeliLucero1 Aug 24, 2026
ce2c22e
docs: trim history comments per review
FeliLucero1 Aug 25, 2026
d9ad380
docs: drop dead cross-reference to Validate()'s trimmed doc comment
FeliLucero1 Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,32 @@ on:
push:
branches:
- main
# Workflow-level (not per-job): all three jobs below hit the same shared DocuSign demo
# account, and running two runs' Grant/Revoke cycles concurrently races on that account's
# real state (one run's mid-cycle Grant/Revoke can make another run's "should be zero
# grants after Revoke" assertion fail). 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. Declaring it once here makes the whole
# needs-chained run (all three jobs) queue/cancel as one unit against the shared group.
# Workflow-level, not per-job: all three jobs share the DocuSign demo account, and a
# per-job block only protects a *running* job — GitHub Actions still cancels a *pending*
# job in the same group when a newer run queues. Declaring it once here queues/cancels
# all three jobs as one unit.
concurrency:
group: docusign-demo-account
cancel-in-progress: false
env:
# This CI account has no CLM subscription, and (unlike a C1-hosted sync) nothing here

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.

let's cleanup this comment 🙏

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, trimmed in ce2c22e.

# filters resource types by OptInRequired — every registered resource type is attempted
# by default. CLM builders now fail the whole sync rather than skip gracefully when CLM
# isn't available (see pkg/connector/clm_roles.go), so the 5 clm_* types must be
# excluded here explicitly to test the ones the three test-* jobs below care about.
# This is an allowlist, not a CLM-only exclusion: if you register a new non-CLM
# resource type in pkg/connector/connector.go, add it here too — otherwise it silently
# gets zero CI sync-test coverage. Declared once at the workflow level (all three jobs
# inherit it) so there's no per-job copy to keep in sync.
#
# signing_group specifically only works here because every job below also sets
# BATON_INCLUDE_SIGNING_GROUPS: "true" — the connector only advertises signing_group
# via ListResourceTypes() when that flag is set (pkg/connector/connector.go), and the
# SDK hard-errors on any filter entry the connector doesn't advertise
# ("invalid resource type 'signing_group' in filter"). If a future job drops that
# per-job env var, it fails with that confusing filter error instead of just skipping
# signing groups.
BATON_SYNC_RESOURCE_TYPES: user,group,permission_profile,signing_group

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: a comma-separated value doesn't reach the SDK as a 4-element list. The SDK reads this via v.GetStringSlice("sync-resource-types") (vendor/.../pkg/cli/commands.go:464), and for an env-sourced value viper returns the raw string (viper.go:1211 — the readAsCSV branch at :1179 only applies when the pflag changed), so cast.ToStringSliceE hits its case string: return strings.Fields(v) (vendor/.../spf13/cast/caste.go:1311) and yields the single element "user,group,permission_profile,signing_group". Two consequences: the sync aborts with invalid resource type '...' in filter (vendor/.../pkg/sync/syncer.go:1284), and SyncFilterIsExplicit() becoming true with no matching ID also flips skipPermissionProfileResourceType to true in pkg/connector/connector.go:226. Whitespace-separating the value works with strings.Fields; passing --sync-resource-types=user,group,... as a flag also works via the CSV branch.

Suggested change
BATON_SYNC_RESOURCE_TYPES: user,group,permission_profile,signing_group
BATON_SYNC_RESOURCE_TYPES: user group permission_profile signing_group

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Traced this further — the readAsCSV branch does apply here, just not through viper's Get() directly. DefineConfigurationV2 (pkg/config/config.go:199) calls cli.VisitFlags(mainCMD, v) right after registering pflags, before cmd.Execute() ever runs. For an env-sourced stringSlice field, VisitFlags's default branch (pkg/cli/cli.go:176-179) does cmd.Flags().Set(f.Name, v.GetString(f.Name)) — feeding the raw comma-separated env string into pflag's own stringSliceValue.Set(), which CSV-splits via encoding/csv (vendor/github.com/spf13/pflag/string_slice.go:22-29,42-54) and marks the flag Changed=true as a side effect. By the time application code calls v.GetStringSlice("sync-resource-types") (commands.go:464 etc.), viper's PFLAG-override branch fires (flag.HasChanged() is now true) and returns the pflag's already-correctly-split value — the raw env string never reaches cast.ToStringSliceE's whitespace-splitting path at all.\n\nConfirmed empirically too: the passing test-groups CI run for this SHA schedules list-resources for exactly the 4 intended types, with zero "invalid resource type" errors anywhere in the log.\n\nSo BATON_SYNC_RESOURCE_TYPES as a comma-separated env var does work as documented — VisitFlags exists specifically to bridge env-sourced values through pflag's CSV parsing before any GetStringSlice() call. No code change needed here.

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: this allowlist has an undocumented coupling to BATON_INCLUDE_SIGNING_GROUPS. signing_group is only advertised by ListResourceTypes() when that flag is set (connector.go:91), and the SDK hard-errors on any filter entry the connector doesn't advertise — validateSyncResourceTypesFilter in pkg/sync/syncer.go:1341 returns invalid resource type 'signing_group' in filter. It works today only because all three jobs happen to set BATON_INCLUDE_SIGNING_GROUPS: "true". Worth adding to the comment above so a future job that drops that per-job env var doesn't fail with a confusing filter error rather than simply skipping signing groups.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch on the undocumented coupling — added the explanation in 78b700e.

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 comment above warns that a newly registered non-CLM resource type silently loses CI sync coverage if it isn't added here, but nothing enforces that. TestNew_IncludeClmDerivation's "CI's actual allowlist" case (connector_test.go) hardcodes the same four IDs independently, so the two lists can drift apart without any test failing. Consider deriving that test case from alwaysRegisteredTypeIDs minus the clm_* entries, so adding a resource type forces this list to be revisited.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, real drift risk. Fixed in 8bd0005: that test case now derives from alwaysRegisteredTypeIDs minus clm_* entries (with signing_group added back separately, since it's intentionally excluded from that var — conditionally registered, not always). Registering a new non-CLM type in alwaysRegisteredTypeIDs now flows into this test automatically.

# Forces the legacy v1/SQLite c1z format instead of baton-sdk v0.25.0's new default
# (Pebble/v3). The `baton` CLI these jobs download (ConductorOne/github-workflows'
# get-baton action, currently v0.4.5) is built against baton-sdk v0.8.24 — long before
Expand Down
37 changes: 29 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ Copy the `code` parameter value and paste it when prompted. Save the refresh tok

DocuSign CLM (Contract Lifecycle Management) is a separate DocuSign product from
eSignature, with its own API and a separate production subscription. CLM members, roles,
groups, folders, folder security, and permission sets sync alongside the standard
eSignature resources, with no config flag to enable — accounts that don't have CLM simply
sync no CLM resources.
groups, folders, folder security, and permission sets are opt-in: they don't sync by
default, and a customer must explicitly enable each CLM resource type in C1's sync
configuration.

Requirements:

Expand All @@ -115,11 +115,32 @@ Requirements:
also be granted the CLM API scopes on ConductorOne's platform side before any CLM data
will sync. Contact ConductorOne if no CLM data appears in this mode.

The 5 CLM resource types are always registered and visible to C1 — this avoids a C1 sync
engine treating CLM resources as deleted if they stop appearing (see
[CHANGE_TYPES.md](CHANGE_TYPES.md) if you're touching this). Without the CLM OAuth scopes
(or without a CLM subscription on the account), each CLM resource type's sync is skipped
gracefully rather than erroring the whole sync.
The 5 CLM resource types are always registered and visible to C1, but each carries
`OptInRequired` — C1 excludes them from a customer's sync by default, and they only run
once a customer explicitly opts in (see [CHANGE_TYPES.md](CHANGE_TYPES.md) if you're
touching this). C1's opt-in toggle does not validate the underlying DocuSign account
first, so a customer can enable CLM sync without actually having the subscription or
scopes above. If that happens, the sync fails loudly rather than silently succeeding
with zero CLM resources — an account that opted in but can't reach CLM is treated as a
misconfiguration to fix (disable the resource type, or activate the CLM feature), not an
expected state to tolerate.
Comment on lines +118 to +126

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: this new fail-loud contract is described purely in terms of C1's opt-in toggle, but OptInRequired is only enforced platform-side — a standalone run (the brew/docker/go install quickstarts at README:171, 199, 221, and the LOG_LEVEL=debug ./baton-docusign --config-file=... debug flow) registers and attempts all 5 CLM builders by default, so on an eSignature-only account the entire sync now fails instead of skipping CLM. This PR's own .github/workflows/ci.yaml change is that case: every sync job needed BATON_SYNC_RESOURCE_TYPES added to keep working. Worth one line here pointing local/self-hosted users at --sync-resource-types (or BATON_SYNC_RESOURCE_TYPES) to exclude clm_* when the account has no CLM, so the workaround isn't discoverable only from the CI file. Medium confidence — behavior is intentional, the gap is that the docs only cover the C1-hosted path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, and it lines up with what we found the hard way earlier in this PR (the CI jobs needed BATON_SYNC_RESOURCE_TYPES added to keep passing, for exactly this reason). Added a paragraph in 21793c9 pointing self-hosted/CLI users at --sync-resource-types/BATON_SYNC_RESOURCE_TYPES to exclude clm_* on an eSignature-only account.


`OptInRequired` is enforced by ConductorOne's platform, not by the connector or baton-sdk
itself — a self-hosted connector running in service mode still has its per-resource-type
`List()` calls filtered by the platform's opt-in selection (applied inside baton-sdk's
syncer, not surfaced to the connector's own code), but running `baton-docusign` directly
as a one-shot CLI sync (the quickstarts below, with no service/task involved at all)
attempts all 5 CLM resource types by default, with no opt-in gate at all. If that account
doesn't have a CLM subscription, the sync now fails instead of skipping CLM gracefully.
Pass `--sync-resource-types` (or `BATON_SYNC_RESOURCE_TYPES`, comma-separated) with the
resource type IDs you actually want (e.g. `user,group,permission_profile`) to exclude
`clm_member,clm_role,clm_group,clm_permission_set,clm_folder` on an eSignature-only
account run this way.

One check does NOT see that platform filter in either deployment mode: `Connector.Validate()`'s
upfront CLM-readiness check runs once, before any resource type's `List()` and before the
platform filter is applied to anything — a known, reviewed, and deliberately accepted gap,
not an oversight.

CLM permission sets sync for visibility only — DocuSign's CLM API has no endpoint to
assign or unassign a permission set, so they cannot be granted or revoked through this
Expand Down
4 changes: 2 additions & 2 deletions docs/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ The Docusign connector supports [automatic account provisioning and deprovisioni

Every Docusign account must be assigned at least one permission profile. If all other permission profiles are revoked, the account will be automatically assigned the **DocuSign Viewer** profile, which cannot be revoked.

*By default, signing groups are not synced. Enable the **Include Signing Groups** setting to sync signing groups, if your account has the feature enabled.
*By default, signing groups are not synced. Enable the **Include Signing Groups** setting to sync signing groups. Once enabled, your account must actually have the signing groups feature — ConductorOne doesn't validate this before letting you turn the setting on, so enabling it without the feature will fail the sync rather than silently sync no signing groups.

**DocuSign CLM (Contract Lifecycle Management) is a separate, separately-licensed DocuSign product. CLM resources sync automatically if the account has a DocuSign CLM production subscription and the credential has been granted the OAuth scopes CLM needs; accounts without CLM simply sync no CLM resources. CLM permission sets sync for visibility only; DocuSign's CLM API has no endpoint to assign or unassign one.
**DocuSign CLM (Contract Lifecycle Management) is a separate, separately-licensed DocuSign product. CLM resources are opt-in — enable each CLM resource type in your sync configuration to turn them on. Once enabled, your DocuSign account must have a CLM production subscription and the credential must have been granted the OAuth scopes CLM needs; enabling a CLM resource type without them will fail the sync rather than silently sync no data, since ConductorOne doesn't validate the underlying subscription before letting you opt in. CLM permission sets sync for visibility only; DocuSign's CLM API has no endpoint to assign or unassign one.

<Warning>
If you use **OAuth Authentication** (the default, managed method), syncing CLM data requires ConductorOne's managed OAuth app to be granted the CLM API scopes on the platform side. If CLM data doesn't appear after setup, contact ConductorOne. This doesn't apply to **Custom App (Demo Environment)**, where the connector requests the CLM scopes directly using your own DocuSign app credentials.
Expand Down
4 changes: 2 additions & 2 deletions docs/doc-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

**Important Note about CLM:**

- CLM (Contract Lifecycle Management) is a separate, separately-licensed DocuSign product with its own API. There is no config flag to enable it: CLM resources sync whenever the account and credential can reach the CLM API, and accounts without CLM sync no CLM resources.
- CLM (Contract Lifecycle Management) is a separate, separately-licensed DocuSign product with its own API. The 5 CLM resource types carry `OptInRequired` and don't sync until a customer explicitly enables them in C1's sync configuration; C1's opt-in toggle doesn't validate the underlying subscription/scopes first, so an account that opts in but can't reach CLM fails the sync loudly rather than silently syncing zero CLM resources.
- Requires a DocuSign CLM production subscription.
- When using ConductorOne's managed OAuth app (the default cloud-hosted authentication method), CLM also requires that managed app to be granted the CLM API scope on ConductorOne's platform side — this is outside the connector's own configuration. Self-hosted or demo-environment setups using a customer-supplied DocuSign app do not have this extra requirement.
- CLM permission sets sync for visibility only; DocuSign's CLM API has no endpoint to assign or unassign one, so they cannot be granted or revoked.
Expand Down Expand Up @@ -171,7 +171,7 @@ DocuSign CLM is a separate, separately-licensed DocuSign product. To sync CLM da

1. Confirm your DocuSign account has a CLM production subscription.
2. Confirm the credential has been granted the CLM OAuth scopes (`spring_read`/`spring_write`).
3. The connector then syncs CLM Members, Roles, Groups, Folders, Folder Security, and Permission Sets automatically — there is no flag to set.
3. The connector then syncs CLM Members, Roles, Groups, Folders, Folder Security, and Permission Sets once a customer explicitly enables each CLM resource type in C1's sync configuration (see the CLM note above — these types carry `OptInRequired`).

If running against ConductorOne's managed OAuth app (the default cloud-hosted
production authentication method), the managed app also needs the CLM API scopes
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
golang.org/x/oauth2 v0.36.0
google.golang.org/grpc v1.83.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down Expand Up @@ -135,7 +136,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260729162451-8efbd57d26e0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,16 @@ func (c *Client) ensureInitialized(ctx context.Context) error {
return nil
}

// EnsureReady exposes the base eSignature-credential check every other client method
// runs internally before its real request, for callers with no eSignature endpoint of
// their own that still need to detect whether the base connection/credentials are
// valid — namely Connector.Validate() (see pkg/connector/connector.go), which runs
// this once, up front, before any resource type's List() executes. Memoized after the
// first successful call, same as every other method — see ensureInitialized.
func (c *Client) EnsureReady(ctx context.Context) error {
return c.ensureInitialized(ctx)
}

// buildClientURL safely reads baseURI and accountId to build a URL.
func (c *Client) buildClientURL(path string, params ...any) (*url.URL, error) {
c.mutex.RLock()
Expand Down
16 changes: 10 additions & 6 deletions pkg/client/clm_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,6 @@ func (c *Client) ensureClmInitialized(ctx context.Context) error {
for k := range raw {
keys = append(keys, k)
}
// codes.FailedPrecondition (not a bare error, which status.Code() would read as
// codes.Unknown): a non-CLM account's discovery response plausibly has a
// different shape entirely (e.g. a bare account object with none of the
// candidate fields), so isOptInFeatureUnavailableError needs a recognizable
// code to tolerate this specific failure the same way it tolerates 401/403 —
// see that function's doc in helper.go.
return status.Errorf(codes.FailedPrecondition, "baton-docusign: CLM account discovery response at %s did not contain a recognized "+
"base-URL field (checked %v); response contained these fields instead: %v", discoveryURL, clmBaseURLCandidateFields, keys)
}
Expand All @@ -223,6 +217,16 @@ func (c *Client) ensureClmInitialized(ctx context.Context) error {
return nil
}

// EnsureClmReady exposes the CLM-readiness check every other CLM client method runs
// internally before its real request, for callers with no CLM endpoint of their own
// that still need to detect CLM availability — namely Connector.Validate() (see
// pkg/connector/connector.go), which runs this once, up front, before any CLM
// builder's List() executes. Memoized after the first successful call, same as every
// other CLM method — see ensureClmInitialized.
func (c *Client) EnsureClmReady(ctx context.Context) error {
return c.ensureClmReady(ctx)
}

// clmExtractBaseURLField scans a CLM account discovery response for the first
// recognized base-URL field, in clmBaseURLCandidateFields priority order. Split out
// from ensureClmInitialized so this defensive-fallback logic can be unit tested
Expand Down
54 changes: 50 additions & 4 deletions pkg/connector/clm_folders.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ import (

var _ connectorbuilder.StaticEntitlementSyncerV2 = (*clmFolderBuilder)(nil)

// The three folder-security principal kinds, as passed to logSkippedFolderSecurityEntry
// and (via the principal_kind field) queryable in logs.
const (
clmFolderPrincipalKindGroup = "group"
clmFolderPrincipalKindRole = "role"
clmFolderPrincipalKindUser = "user"
)

// The 5 grantable Baton entitlement slugs for CLM folder security, in ascending order
// of access.
const (
Expand Down Expand Up @@ -74,10 +82,6 @@ func (f *clmFolderBuilder) List(ctx context.Context, _ *v2.ResourceId, attr rs.S
PageToken: pageToken,
})
if err != nil {
if attr.PageToken.Token == "" && isOptInFeatureUnavailableError(err) {
ctxzap.Extract(ctx).Info("baton-docusign: CLM is not available for this account or token, skipping clm_folder sync", zap.Error(err))
return nil, &rs.SyncOpResults{}, nil
}
return nil, nil, err
}

Expand Down Expand Up @@ -141,6 +145,8 @@ func (f *clmFolderBuilder) Grants(ctx context.Context, folderResource *v2.Resour
for _, entry := range folder.Security.Groups {
slug, ok := clmSlugForAccessType(entry.AccessType)
if !ok {
logSkippedFolderSecurityEntry(ctx, clmFolderPrincipalKindGroup, entry.AccessType,
zap.String("folder_id", folderResource.Id.Resource), zap.String("group_href", entry.Href))
continue
}
principalID := &v2.ResourceId{ResourceType: clmGroupResourceType.Id, Resource: clmIDFromHref(entry.Href)}
Expand All @@ -156,12 +162,17 @@ func (f *clmFolderBuilder) Grants(ctx context.Context, folderResource *v2.Resour
for _, entry := range folder.Security.Roles {
slug, ok := clmSlugForAccessType(entry.AccessType)
if !ok {
logSkippedFolderSecurityEntry(ctx, clmFolderPrincipalKindRole, entry.AccessType,
zap.String("folder_id", folderResource.Id.Resource), zap.String("role", entry.Item))
continue
}
if !clmIsKnownRole(entry.Item) {
// clm_role is a fixed, hardcoded 5-role list (clmRoleBuilder.List) — a role
// name outside that set has no synced principal to grant against. Skip
// rather than emit a grant to a dangling/unsynced resource.
ctxzap.Extract(ctx).Debug("baton-docusign: skipping CLM folder role-security entry for an unrecognized role",
zap.String("folder_id", folderResource.Id.Resource), zap.String("role", entry.Item), zap.String("access_type", entry.AccessType),
zap.String("principal_kind", clmFolderPrincipalKindRole))
continue
}
principalID := &v2.ResourceId{ResourceType: clmRoleResourceType.Id, Resource: entry.Item}
Expand All @@ -171,6 +182,8 @@ func (f *clmFolderBuilder) Grants(ctx context.Context, folderResource *v2.Resour
for _, entry := range folder.Security.Users {
slug, ok := clmSlugForAccessType(entry.AccessType)
if !ok {
logSkippedFolderSecurityEntry(ctx, clmFolderPrincipalKindUser, entry.AccessType,
zap.String("folder_id", folderResource.Id.Resource), zap.String("member_href", entry.Href))
continue
}
principalID := &v2.ResourceId{ResourceType: clmMemberResourceType.Id, Resource: clmIDFromHref(entry.Href)}
Expand All @@ -180,6 +193,28 @@ func (f *clmFolderBuilder) Grants(ctx context.Context, folderResource *v2.Resour
return grants, &rs.SyncOpResults{Annotations: annos}, nil
}

// logSkippedFolderSecurityEntry Debug-logs an unmapped folder-security AccessType.
// Benign values (NoAccess / Inherit) return without logging; Custom logs as an
// unrepresentable active grant. kind is "group"|"role"|"user".
func logSkippedFolderSecurityEntry(ctx context.Context, kind, accessType string, fields ...zap.Field) {
if clmIsBenignUnmappedAccessType(accessType) {
// The common steady-state case (NoAccess/InheritFromParentFolder, on every
// folder of every sync) — return before this function's own append/log call.
// The caller's fields are already built by this point regardless.
return
}
fields = append(fields, zap.String("principal_kind", kind), zap.String("access_type", accessType))
Comment thread
FeliLucero1 marked this conversation as resolved.
if accessType == client.ClmAccessTypeCustom {
ctxzap.Extract(ctx).Debug(
"baton-docusign: skipping CLM folder security entry with an unrepresentable Custom AccessType — a real, active grant C1 won't see",
fields...)
return
Comment on lines +207 to +211

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 comments argue Custom must not be silenced because it's "a real, active grant C1 won't see" — an actual access-visibility gap — but it's logged at Debug, which is off in normal production runs, so in practice it stays invisible anyway. Consider Info/Warn for the Custom branch (volume is bounded by folder-security entries with Custom, not per-sync noise like NoAccess/Inherit) and leave the merely-unmapped branch at Debug. (confidence: medium)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same finding as the reply at #64 (comment) (clm_folders.go, since shifted to this line): deliberate, standing decision, not an oversight. Warn/Info were both explicitly considered and rejected earlier in this PR for this per-entry AccessType-skip logging — Warn as too costly/invisible at this volume, Info since customers don't see Info either and the error text already carries the "why" once something is actually unavailable. Custom stays at Debug, consistent with the rest of this helper's skip logging. No code change.

Comment on lines +207 to +211

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: This branch's own doc says a Custom entry is "a real, active grant C1 won't see" and that silencing it "would hide an actual access-visibility gap" — but Debug is below the connector's default info log level, so in practice it stays hidden anyway. This repo's own guidance (.claude/skills/connector/patterns-error-handling.md quick-reference: "Skip item + continue → Warn — graceful degradation, partial data") points at Warn for exactly this case, while Debug is the right level for the genuinely-unrecognized-enum branch below it. Consider raising just the Custom line to Warn so the stated intent holds at default verbosity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same standing decision — see #64 (comment) just above and #64 (comment): Warn/Info were already considered and explicitly rejected earlier in this PR for this per-entry logging. patterns-error-handling.md's "Warn" quick-reference is a general default for skip-and-continue, not an override of a decision already made for this specific, high-volume, per-entry case. Custom stays at Debug. No code change.

}
Comment thread
FeliLucero1 marked this conversation as resolved.
ctxzap.Extract(ctx).Debug(
"baton-docusign: skipping CLM folder security entry with an unmapped AccessType",
fields...)
}

// Grant sets a folder-security entry for the principal at the entitlement's tier.
// Read-before-write: fetches the folder's current complete security state, modifies
// only the one entry belonging to this principal (in whichever of Groups/Roles/Users
Expand Down Expand Up @@ -433,6 +468,17 @@ func clmSlugForAccessType(accessType string) (string, bool) {
return "", false
}

// clmIsBenignUnmappedAccessType is true for NoAccess and InheritFromParentFolder —
// inert AccessTypes Grants() skips without logging. Custom is not benign.
func clmIsBenignUnmappedAccessType(accessType string) bool {
switch accessType {
case client.ClmAccessTypeNoAccess, client.ClmAccessTypeInherit:
return true
default:
return false
}
}
Comment thread
FeliLucero1 marked this conversation as resolved.

// clmIsKnownRole reports whether name is one of the 5 fixed CLM account-level roles
// (client.ClmRoles) — the same fixed set clmRoleBuilder.List syncs as clm_role
// resources. Used to reject a folder-security Roles entry referencing a role outside
Expand Down
Loading
Loading