feat: add provisioning to add/remove users to Jamf user groups#25
Open
joshblease wants to merge 2 commits into
Open
feat: add provisioning to add/remove users to Jamf user groups#25joshblease wants to merge 2 commits into
joshblease wants to merge 2 commits into
Conversation
Implement Grant/Revoke on the userGroup resource type so ConductorOne can
add and remove end users to/from Jamf "User Groups".
- Add write support to the Jamf client (doPutXML helper mirroring
doRequest's token keep-alive and one-shot re-auth), plus
AddUserToUserGroup / RemoveUserFromUserGroup using the Classic API
PUT /JSSResource/usergroups/id/{id} with <user_additions>/<user_deletions>
XML bodies.
- Implement Grant/Revoke on userGroupResourceType, validating the principal
type and rejecting smart groups (criteria-based membership can't be edited).
- Set WithExternalID during user sync so provisioning can resolve the native
Jamf user ID.
- Regenerate baton_capabilities.json (userGroup now advertises
CAPABILITY_PROVISION).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Jamf keep-alive endpoint requires a still-valid token, so once the bearer token fully expires (e.g. after an idle period between syncs, or before a sporadic provisioning task) keep-alive returns 401 and the token cannot be refreshed. keepAliveToken previously propagated that 401, so the request failed with "401 Unauthorized" — the re-auth-from-credentials fallback only existed in the request path, which was never reached. Now keepAliveToken mints a fresh token via CreateBearerToken when keep-alive fails, so calls transparently recover. This surfaced as grant failures: "failed to get user group details: 401 Unauthorized" after the connector sat idle, while frequent sync bursts masked it. Adds a white-box unit test (the repo's first) reproducing the expired-token path against an httptest server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implements Grant/Revoke provisioning so ConductorOne can add and remove end users to/from Jamf "User Groups" (the
userGroupresource type). The connector was previously read-only — no builder implemented provisioning and the Jamf client only issuedGETrequests.Scope is User Groups only (members are
userresources). Admin account Groups (group.go) are intentionally out of scope.How
pkg/jamf/client.go— adds write support viadoPutXML, a PUT+XML helper that mirrors the existingdoRequesttoken keep-alive and one-shot re-auth onUnauthenticated. AddsAddUserToUserGroup/RemoveUserFromUserGrouptargeting the Classic APIPUT /JSSResource/usergroups/id/{id}with<user_additions>/<user_deletions>bodies.pkg/jamf/models.go— adds theUserGroupMembershipUpdateXML request body. Pointer-wrappeduser_additions/user_deletionselements withomitemptyso only the used element is serialized.pkg/connector/userGroup.go— implementsGrantandRevokeonuserGroupResourceType. Validates the principal is auser, resolves the native Jamf user ID, and rejects smart groups (their membership is criteria-based and cannot be edited via the API).pkg/connector/user.go+helpers.go— setsWithExternalIDduring user sync and adds anativeUserIDresolver (prefersExternalId, falls back to the resource ID) so provisioning can obtain the native user ID.baton_capabilities.json— regenerated;userGroupnow advertisesCAPABILITY_PROVISION. Provisioning is auto-derived by the SDK from the implemented interface, so noMetadata()change is required.Grant/Revoke are naturally idempotent: Jamf treats adding an existing member (or deleting a non-member) as a successful no-op — the incremental
<user_additions>/<user_deletions>writes only touch the target user, leaving other members untouched.Bug fix: recover from an expired Jamf token
While validating against a live deployment, sporadic provisioning after an idle period failed with
failed to get user group details: 401 Unauthorized. Root cause: Jamf's keep-alive endpoint requires a still-valid token, so once the bearer token fully expires (e.g. between infrequent syncs, or before an on-demand grant) keep-alive returns 401 andkeepAliveTokenpropagated it — the re-auth-from-credentials fallback only existed in the request path and was never reached. Frequent sync bursts kept the token warm and masked this.pkg/jamf/client.go—keepAliveTokennow mints a fresh token viaCreateBearerTokenwhen keep-alive fails, so any call (sync or provisioning) transparently recovers from an expired token.pkg/jamf/client_test.go— adds the repo's first unit test, reproducing the expired-token path against anhttptestserver (keep-alive 401 → re-auth → request retried with the fresh token).Manual verification
Driven through the real binary against a live Jamf Pro instance, using a static user group ("Aikido Endpoint Protection", 27 existing members) and a test user, verified out-of-band via the Classic API:
--grant-entitlement 'userGroup:<id>:member' --grant-principal '<userId>' --grant-principal-type useradds the user (group 27 → 28 members; target user now present; other 27 members untouched).--revoke-grant 'userGroup:<id>:member:user:<userId>'removes the user (28 → 27; target user gone; others untouched).jamf-connector: cannot modify membership of smart user group "..." (membership is determined by criteria)and writes nothing../baton-jamf capabilitiesshowsuserGroupadvertisingCAPABILITY_PROVISION(matches the regeneratedbaton_capabilities.json).Production tenant
Additionally deployed and exercised end-to-end on a production ConductorOne tenant (self-hosted connector running as a systemd service on a VM). Full syncs (resources + entitlements + grants, including
userGroupmemberentitlements) complete and upload cleanly, and the provisioning path was driven from the C1 UI. The expired-token 401 above was first observed here after the connector sat idle, and the fix was verified against the same tenant.Test plan
go build ./...,go vet ./pkg/...,gofmtcleango test ./...— passing (expired-token recovery unit test)baton_capabilities.jsonverified semantically identical to the SDKcapabilitiesoutput🤖 Generated with Claude Code