-
Notifications
You must be signed in to change notification settings - Fork 0
[CXH-2209] fix: gate clm_role on CLM availability, narrow opt-in error tolerance, log skipped folder-security entries #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b339b0d
1863069
b002b0b
091f923
6b4a8ed
d0661b6
aec3e95
fcf849b
96aaebf
3ac142e
b446fe7
6ff1642
7ef387f
7c97d95
39312ee
241ad3d
b48a9d8
fa73fbc
03901dc
6bbf8a0
31aae8a
e988e24
940dd66
a69026f
01b398f
de6d113
4c11459
151555c
72d9199
15d592c
efba5fb
f56957e
760b67e
60751be
09573b2
49e2e6c
e2889b7
8cc9abf
f31ec4c
668a17b
dac1f92
d2f3a3e
85a5fdd
5edc9ee
9ecc8d6
1b93916
023b3c2
c7d5496
16a9ef1
8bd0005
15264e5
ad17858
ce2c22e
d9ad380
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
| # 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: this allowlist has an undocumented coupling to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch on the undocumented coupling — added the explanation in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, real drift risk. Fixed in |
||||||
| # 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 | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
|
||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ( | ||
|
|
@@ -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 | ||
| } | ||
|
|
||
|
|
@@ -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)} | ||
|
|
@@ -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} | ||
|
|
@@ -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)} | ||
|
|
@@ -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)) | ||
|
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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: the comments argue
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same finding as the reply at #64 (comment) (
Comment on lines
+207
to
+211
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: This branch's own doc says a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
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 | ||
|
|
@@ -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 | ||
| } | ||
| } | ||
|
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 | ||
|
|
||
There was a problem hiding this comment.
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 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, trimmed in ce2c22e.