Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
24f7a74
fix: gate clm_role on CLM availability, narrow opt-in error tolerance…
FeliLucero1 Aug 7, 2026
b3b9cef
fix: revert opt-in-error narrowing, log instead of gate on error source
FeliLucero1 Aug 7, 2026
c88b9d7
fix: address current bot review findings on PR #64
FeliLucero1 Aug 10, 2026
11f67ab
fix: suppress expected-state Debug logs diluting the AccessType skip …
FeliLucero1 Aug 10, 2026
df1fdc3
fix: narrow clm_role's CLM-availability tolerance to discovery errors…
FeliLucero1 Aug 10, 2026
af86078
fix: require isOptInFeatureUnavailableError alongside IsClmDiscoveryE…
FeliLucero1 Aug 10, 2026
02e08f5
fix: clmIsBenignUnmappedAccessType doc/test correctness (no log-level…
FeliLucero1 Aug 10, 2026
9142319
fix: pin which entry logs in the AccessType-skip test, not just how many
FeliLucero1 Aug 10, 2026
133e237
test: cover the other half of clm_role's two-condition CLM-availabili…
FeliLucero1 Aug 10, 2026
1560cf2
fix: carry CLM-discovery source as a log field, not a log level
FeliLucero1 Aug 10, 2026
edb8520
fix: drop clmDiscoverySourceField — the error text already says why
FeliLucero1 Aug 10, 2026
e785944
test: cover Roles/Users branches for the AccessType-skip log; fix For…
FeliLucero1 Aug 11, 2026
0a45d52
test: assert each branch's distinguishing log field, not just count/a…
FeliLucero1 Aug 11, 2026
61077d0
fix: address remaining PR #64 review threads
FeliLucero1 Aug 11, 2026
79bd6aa
test: bind unmapped-AccessType log assertions per-message, not set-cover
FeliLucero1 Aug 11, 2026
47d1d01
refactor: dedupe folder-security skip logging into one helper
FeliLucero1 Aug 11, 2026
9ba3ffe
fix: add missing access_type to unrecognized-role skip log; harden sa…
FeliLucero1 Aug 11, 2026
6731ebc
perf: stop building a per-kind message via fmt.Sprintf on every folde…
FeliLucero1 Aug 11, 2026
f858e1e
fix: close two gaps left by the previous skip-log pass
FeliLucero1 Aug 11, 2026
4db726a
fix: resolve duplicate sample-branch re-seed logic from rebase conflict
FeliLucero1 Aug 12, 2026
2a85833
fix: apply the discovery-source two-conjunct gate to the other 4 CLM …
FeliLucero1 Aug 12, 2026
8ea8394
revert: pull back the discovery-source gate from the 4 data-backed CL…
FeliLucero1 Aug 12, 2026
c63f7f7
fix: fail loudly instead of silently skipping when an opt-in feature …
FeliLucero1 Aug 12, 2026
ada60a7
fix: exclude CLM/signing-group risk from CI by pinning sync-resource-…
FeliLucero1 Aug 13, 2026
23d8611
refactor: drop dead CLM-discovery-error machinery, fix signing_group …
FeliLucero1 Aug 13, 2026
1393956
fix: drop dead conjunct and fix stale comments in folder-security log…
FeliLucero1 Aug 13, 2026
6234969
docs: warn self-hosted/CLI users that OptInRequired doesn't protect them
FeliLucero1 Aug 13, 2026
cf270c1
test: add signing_groups_test.go, covering the new fail-loud behavior
FeliLucero1 Aug 13, 2026
b3cee19
fix: address bot review findings on PR #64, dedupe rewriteTransport
FeliLucero1 Aug 13, 2026
1e0c53f
docs: warn maintainers that CI's resource-type allowlist needs updating
FeliLucero1 Aug 13, 2026
f11def5
fix: serialize CI jobs that hit the shared DocuSign demo account
FeliLucero1 Aug 13, 2026
7eef9d0
fix: restore grpc codes/status imports dropped by the PR #64 rebase
FeliLucero1 Aug 13, 2026
70405d2
docs: dedupe BATON_SYNC_RESOURCE_TYPES into a workflow-level env block
FeliLucero1 Aug 13, 2026
96e7fa2
fix: centralize CLM and base-credential checks in Connector.Validate()
FeliLucero1 Aug 18, 2026
c11bddc
docs: narrow README's self-hosted/CLI language to the one-shot CLI case
FeliLucero1 Aug 18, 2026
57c2f09
fix: hoist ci.yaml's per-job concurrency block to workflow level
FeliLucero1 Aug 18, 2026
e125bd7
fix: correct stale signingGroupResourceType registration comment
FeliLucero1 Aug 18, 2026
6f9251b
fix: drop NewWithClient's self-contradictory compatibility rationale
FeliLucero1 Aug 18, 2026
18d1a24
fix: delete dead NewWithClient constructor and its test
FeliLucero1 Aug 18, 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
14 changes: 13 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,23 @@ on:
# 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
# same group when a newer one queues, so two overlapping workflow runs could cancel each
# other's pending jobs mid-chain. Declaring it once here instead makes the whole
# needs-chained run (all three jobs) queue/cancel as one unit against the shared group.
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
# 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.
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.

jobs:
test-groups:
runs-on: ubuntu-latest
Expand Down
31 changes: 23 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,26 @@ 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 receives the platform's
resource-type filter, 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

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: same parsing caveat as the CI workflow — BATON_SYNC_RESOURCE_TYPES is read through v.GetStringSlice, which for an env-sourced string splits on whitespace (cast.ToStringSliceEstrings.Fields), not commas. Documented as comma-separated, an eSignature-only operator following this instruction gets a single bogus filter entry and the sync fails with invalid resource type '...' in filter rather than excluding the CLM types. Commas are correct for the --sync-resource-types flag form (pflag CSV parsing); for the env var, document a space-separated value.

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 mechanism as the ci.yaml thread applies here. VisitFlags (pkg/cli/cli.go:152-180), invoked from DefineConfigurationV2 before cmd.Execute(), routes an env-sourced stringSlice value through pflag's own Set() — which CSV-splits on commas (vendor/github.com/spf13/pflag/string_slice.go:22-29,42-54) — before any GetStringSlice() call happens, and marks the pflag Changed so viper's PFLAG-override branch (not the raw-env-string path) is what actually gets consumed downstream. So the comma-separated env-var form documented here is correct as written. No doc change needed.

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.

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
2 changes: 1 addition & 1 deletion 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
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 @@ -184,12 +184,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 @@ -199,6 +193,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
72 changes: 68 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.Items {
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.Items {
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.Items {
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,35 @@ func (f *clmFolderBuilder) Grants(ctx context.Context, folderResource *v2.Resour
return grants, &rs.SyncOpResults{Annotations: annos}, nil
}

// logSkippedFolderSecurityEntry logs the one Debug line for a folder-security entry

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.

[Docs / nit] logSkippedFolderSecurityEntry's doc narrates Sprintf removal, field vs message tradeoffs, and Custom vs benign — more than the helper needs.

Suggested trim:

// 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".

// whose AccessType didn't map to a grantable tier — shared by the Groups/Roles/Users
// branches of Grants, which differ only in kind ("group"/"role"/"user", carried as a
// field rather than interpolated into the message, so both messages stay constant
// strings — no per-call fmt.Sprintf) and the caller-supplied fields identifying the
// entry. Custom gets its own message, since unlike NoAccess/InheritFromParentFolder
// (clmIsBenignUnmappedAccessType) it's a real, active grant this connector can't
// represent — fully silencing it would hide an actual access-visibility gap. Both
// branches carry access_type so either case is findable by the same structured-log
// query as every other skip line in this file.
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 +214 to +218

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 +214 to +218

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 +475,28 @@ func clmSlugForAccessType(accessType string) (string, bool) {
return "", false
}

// clmIsBenignUnmappedAccessType reports whether accessType is one of the two documented

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.

[Docs / nit] Same over-explaining pattern: Revoke/Inherit history and Custom contrast belong in logSkippedFolderSecurityEntry (or nowhere) — this predicate only needs what it returns.

Suggested trim:

// clmIsBenignUnmappedAccessType is true for NoAccess and InheritFromParentFolder —
// inert AccessTypes Grants() skips without logging. Custom is not benign.

// non-grantable-but-truly-inert values every folder-security entry can legitimately
// carry — NoAccess (this connector's own Revoke leaves entries in place at this value,
// so it appears on every subsequent sync of a revoked entry) and InheritFromParentFolder
// (an absence-of-override marker — see clmFolderEntitlement's doc). Grants() skips these
// the same way it skips Custom, but stays fully silent for them, unlike Custom: neither
// represents an access grant C1 is failing to show, so logging them would only add
// per-sync noise for two expected states large accounts can produce on every sync.
//
// Custom is deliberately NOT in this set — see its own Debug log in
// logSkippedFolderSecurityEntry: it's a real, active grant this connector can't
// round-trip to a single tier (an arbitrary flag combination), so silencing it the same
// way would hide an actual access-visibility gap, not just an expected inert state.
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