Skip to content

[CXP-1240][CXP-1286] Reduce and improve the cost and speed of synchronization, and fix users shown as "user" - #35

Open
mateovespConductor wants to merge 4 commits into
mainfrom
mateovesp/cxp-1240-sync-cost
Open

mateovespConductor wants to merge 4 commits into
mainfrom
mateovesp/cxp-1240-sync-cost

Conversation

@mateovespConductor

@mateovespConductor mateovespConductor commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Cuts the sync from one Classic detail read per item (and per role and per site) to a handful of paged Jamf Pro API calls. Mock tenant: 2,125 requests -> 31. Trial tenant (1,114 users, 22 Custom accounts): 50 requests, 43 s, 0 errors. Also fixes CXP-1286 (users shown as user) and resolves CXP-1285 (duplicate of CXP-1240).

What changed

  • Users, admin accounts and site members come from the Jamf Pro API, paged by the SDK. Account groups and Custom-account privileges stay on the Classic API (least-privilege installs lack Read Account Groups).
  • role is type-scoped (TypeScopedGrants): one pass over the accounts, then the Custom accounts at 10 per page with one detail read each. Group role grants are emitted from group Grants.
  • group, userGroup, site and role use StaticEntitlements + SkipEntitlements.
  • Every Grants response stays under gRPC's 4 MiB limit: group pages of 1000 grants (keyset cursor), user group pages of 1000 members, role pages of up to 10 Custom accounts.
  • userGroup Grants read the group once; later pages come from the session store (1000 member ids per key).
  • Cross-call state lives in the session store (privilege catalog, username/email -> user id, user group member pages). No new in-process caches.
  • A 404 from a Pro endpoint is FailedPrecondition, not NotFound: a NotFound from List/Grants makes the SDK skip the action and C1 deletes the data. Requires Jamf Pro 11.26.0 or later (README, connector.mdx; docs-info lists each endpoint's version).
  • Ticket items: InvalidArgument on bad CreateAccount/Delete input, Delete refuses the connector's own account, userAccount display name from FullName, site errors with context, getUserIndex lock removed with the index.

Visible in C1 (no ID changes)

  • Resource, entitlement and grant IDs, slugs, traits and parents are identical to main (mock and trial c1z diffs).
  • Entitlement display name is now "Member" on group/userGroup/site/role; uplift copies it onto existing entitlements unless an admin edited them. C1 still lists them by resource name ("{resource} — member"). userAccount display names show the full name; roles gain a description.
  • User display names fall back to the username, then the email, when Jamf has no full name (CXP-1286). On the trial, 1,101 of 1,114 users had no name before.
  • Fewer privileges: site sync no longer needs Read User or user group reads, and managed devices no longer need Read User.
  • A managedDevice-only sync now emits device grants with ExternalResourceMatch (main emitted none after the SDK ingest filter).

Deviations from the ticket

  • Validate is unchanged (token check only): the extra read was dropped.
  • The empty-group retry loop is removed, not kept with fresh reads: it replayed the cached response, and an empty read for a populated group doesn't reproduce on a real tenant. Grant/Revoke still refuse to write over an empty list.
  • No parallel reads: Pro paging removed the need; Custom-account detail reads stay serial (1 of 49 connectors fans out).

Known, pre-existing, deferred to CXP-1241 (device work; ships with this before CXP-1217)

  • deviceOwners is an in-process map filled in List: on Lambda, Entitlements/Grants run in other invokes and emit nothing. Fix: assignee on the device profile.
  • Whitespace-only assignee reaches grant.NewGrant with a nil id (panic).
  • Device Pro endpoints still return NotFound on 404; per-device lookup when user is not synced; always emit assigned.
  • c1 still runs Jamf in-process at v0.0.9 without a session store; this version targets the Lambda rollout (CXP-1217).

@mateovespConductor
mateovespConductor requested a review from a team October 30, 2026 20:29
@linear-code

linear-code Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

CXP-1240

CXP-1286

CXP-1285

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 491af9191562

Connector PR Review: [CXP-1240] Reduce and improve the cost and speed of synchronization

Blocking Issues: 0 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: full
View review run

Review Summary

The PR replaces the Classic API's one-detail-read-per-item sync with paged Jamf Pro API calls:

  • Users and accounts: /api/v1/users and /api/v1/accounts now back user and userAccount List.
  • Site grants: come from /api/v1/sites/{id}/objects, the admin accounts filtered by siteId, and group Grants.
  • Role grants: are now type-scoped. Privilege sets come from the accounts list; Custom accounts are read 10 per page. Group role grants and group site grants are emitted from group Grants.
  • Entitlements: group/userGroup/site/role use StaticEntitlements.
  • Cross-call state: device-owner resolution and the privilege catalog now live in the session store (WithSessionStoreEnabled).

I scanned the full diff for security and correctness. Applying the repo-local criteria:

  • S5/S6: the only per-item skip is customPrivilegeGrants on a 404, and the client doc comment on GetUserAccountDetails states that a 404 means the account is absent, so the skip is allowed.
  • S1: Pro-endpoint 404s are mapped to FailedPrecondition, so missing endpoints fail the sync instead of emitting empty results.
  • E1/E2: wrap codes are correct.
  • L1–L7: log levels are correct.
  • Sections C/D: no spans added. New id,string JSON IDs are acceptable.
  • Breaking changes (B/BP): no resource IDs, slugs or parents changed.
  • B10: specs/openapi.json was updated for the three new endpoints.

Coverage limits:

  • I reviewed the test files and test-server/ only lightly; they are not production paths.
  • I could not verify Jamf's actual behavior: RSQL case sensitivity, privilegeLevel for Group Access accounts, and whether the page-size=1000 cap on site objects is honored. I relied on the spec annotations in the PR.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New — pkg/jamf/client.go:37 (confidence high, severity suggestion): user sync, and the site and role grants, now depend on Jamf Pro API list endpoints with no Classic fallback. Per the PR body, /api/v1/users needs Jamf Pro ≥ 11.26, so older instances will fail every sync with FailedPrecondition. The PR body says the README documents this, but neither README.md nor docs/connector.mdx mentions 11.26.
  • New — pkg/connector/group.go:110 (confidence medium): the retry for Jamf's documented empty-members bug on /JSSResource/accounts/groupid/{id} was removed. One transient empty response now emits zero member grants, which C1 treats as revoking every member of the group.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/jamf/client.go`:
- Around line 37 (proUsersUrlPath / GetProUsers, GetProAccounts, GetSiteObjects): sync now requires Jamf Pro API list endpoints (/api/v1/users needs Jamf Pro >= 11.26 per the PR description). Older instances fail every sync with FailedPrecondition. Add the minimum Jamf Pro version to README.md and docs/connector.mdx (the PR body claims the README has it, but it does not). Consider a Classic API fallback, or gating the new endpoints, for older versions.

In `pkg/connector/group.go`:
- Around line 110 (groupGrants -> GetGroupDetails): the workaround for Jamf intermittently returning an empty members list from /JSSResource/accounts/groupid/{id} was removed. An empty response now produces zero member grants, which C1 treats as revoking every member. Either document evidence that the Jamf bug is fixed, or cross-check an empty member list before emitting it, e.g. by re-reading, or by checking /api/v1/accounts for accounts belonging to the group.

Comment thread pkg/jamf/client.go
userNameUrlPath = "/JSSResource/users/name/%s" // https://developer.jamf.com/jamf-pro/reference/findusersbyname
oauthTokenUrlPath = "/api/v1/oauth/token" //nolint:gosec // not a token, https://developer.jamf.com/jamf-pro/docs/client-credentials
privilegesUrlPath = "/api/v1/api-role-privileges" // https://developer.jamf.com/jamf-pro/reference/get_v1-api-role-privileges
proUsersUrlPath = "/api/v1/users" // https://developer.jamf.com/jamf-pro/reference/get_v1-users

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 (confidence: high that it's real, medium on severity): User List, and the user/admin grants in site and role, now go through /api/v1/users, /api/v1/accounts and /api/v1/sites/{id}/objects with no fallback. Per the PR body, /api/v1/users needs Jamf Pro ≥ 11.26, so an older on-prem instance fails every sync with FailedPrecondition after this deploys. The PR body says the README documents this, but neither README.md nor docs/connector.mdx mentions 11.26. Please document the minimum version, and consider falling back to the Classic list or gating the change.

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.

+1. I grepped README, connector.mdx and docs-info.md for 11.26 and found nothing, even though the PR body says it's in the README. docs-info.md:70 still says no endpoint is gated. The minimums for /api/v1/accounts and /sites/{id}/objects aren't documented anywhere, and user, userAccount, site and role sync all depend on them now.

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.

Thanks, fixed in fe05279: README and connector.mdx state the minimum, 11.26.0, and docs-info lists each endpoint's version (users 11.26.0, accounts 11.20.0, site objects 11.2.0). No Classic fallback, since it would bring back the per-item reads this PR removes.

Comment thread pkg/connector/group.go Outdated
time.Sleep(time.Second)
// groupGrants lists the grants of one group: its members, then the roles and the site it holds.
func (g *groupResourceType) groupGrants(ctx context.Context, store sessions.SessionStore, resource *v2.Resource, groupId int) ([]*v2.Grant, error) {
group, err := g.client.GetGroupDetails(ctx, groupId)

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 (confidence: medium): The removed loop worked around a documented Jamf bug: /JSSResource/accounts/groupid/{id} sometimes returns an empty members list for a group that has members. Without it, one empty response now makes Grants() return zero member grants, which C1 treats as revoking every member of that group (section S data-loss shape, though no error is swallowed here). If the bug is confirmed fixed, cite the evidence in a comment. Otherwise consider cross-checking an empty list, e.g. by retrying, or by checking the Pro /api/v1/accounts list for accounts in the group, before emitting an empty membership.

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.

+1. "Members come from the group detail" is the same /JSSResource/accounts/groupid/{id} endpoint the original HACK (922cc5b, BB-398) was about, so the bug is still there. To be fair, the old loop never re-read fresh (no WithFreshReads), so it wasn't really protecting anything either. But the ticket asked to keep the retry with fresh reads and respect ctx, and Grant/Revoke plus the README still treat an empty read as untrustworthy. 1-2 fresh re-reads on empty would close it.

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.

Thanks for digging into the history! We're keeping it out: an empty read for a populated group doesn't reproduce on a real tenant, and we don't keep retry loops in sync for vendor flakiness. Grant/Revoke still refuse to write over an empty list, which is where a bad read could actually drop members.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 2e632aca3b49

Connector PR Review: [CXP-1240] Reduce and improve the cost and speed of synchronization

Blocking Issues: 0 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: full
View review run

Review Summary

The PR replaces the Classic API's one-detail-read-per-item sync with paged Jamf Pro API calls:

  • Users and accounts: /api/v1/users and /api/v1/accounts now back user and userAccount List.
  • Site grants: come from /api/v1/sites/{id}/objects, the admin accounts filtered by siteId, and group Grants.
  • Role grants: are now type-scoped. Privilege sets come from the accounts list; Custom accounts are read 10 per page. Group role grants and group site grants are emitted from group Grants.
  • Entitlements: group/userGroup/site/role use StaticEntitlements.
  • Cross-call state: device-owner resolution and the privilege catalog now live in the session store (WithSessionStoreEnabled).

I scanned the full diff for security and correctness. Applying the repo-local criteria:

  • S5/S6: the only per-item skip is customPrivilegeGrants on a 404, and the client doc comment on GetUserAccountDetails states that a 404 means the account is absent, so the skip is allowed.
  • S1: Pro-endpoint 404s are mapped to FailedPrecondition, so missing endpoints fail the sync instead of emitting empty results.
  • E1/E2: wrap codes are correct.
  • L1–L7: log levels are correct.
  • Sections C/D: no spans added. New id,string JSON IDs are acceptable.
  • Breaking changes (B/BP): no resource IDs, slugs or parents changed.
  • B10: specs/openapi.json was updated for the three new endpoints.

Coverage limits:

  • I reviewed the test files and test-server/ only lightly; they are not production paths.
  • I could not verify Jamf's actual behavior: RSQL case sensitivity, privilegeLevel for Group Access accounts, and whether the page-size=1000 cap on site objects is honored. I relied on the spec annotations in the PR.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New — pkg/jamf/client.go:37 (confidence high, severity suggestion): user sync, and the site and role grants, now depend on Jamf Pro API list endpoints with no Classic fallback. Per the PR body, /api/v1/users needs Jamf Pro ≥ 11.26, so older instances will fail every sync with FailedPrecondition. The PR body says the README documents this, but neither README.md nor docs/connector.mdx mentions 11.26.
  • New — pkg/connector/group.go:110 (confidence medium): the retry for Jamf's documented empty-members bug on /JSSResource/accounts/groupid/{id} was removed. One transient empty response now emits zero member grants, which C1 treats as revoking every member of the group.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/jamf/client.go`:
- Around line 37 (proUsersUrlPath / GetProUsers, GetProAccounts, GetSiteObjects): sync now requires Jamf Pro API list endpoints (/api/v1/users needs Jamf Pro >= 11.26 per the PR description). Older instances fail every sync with FailedPrecondition. Add the minimum Jamf Pro version to README.md and docs/connector.mdx (the PR body claims the README has it, but it does not). Consider a Classic API fallback, or gating the new endpoints, for older versions.

In `pkg/connector/group.go`:
- Around line 110 (groupGrants -> GetGroupDetails): the workaround for Jamf intermittently returning an empty members list from /JSSResource/accounts/groupid/{id} was removed. An empty response now produces zero member grants, which C1 treats as revoking every member. Either document evidence that the Jamf bug is fixed, or cross-check an empty member list before emitting it, e.g. by re-reading, or by checking /api/v1/accounts for accounts belonging to the group.

Reviewed commit: 491af9191562

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

- List users, admin accounts and site members through the Jamf Pro API, SDK-paged (1000 / 100 / 1000 per page)
- Role grants type-scoped: privilege-set phase, then Custom accounts (10 per page, one detail read each)
- Group grants from one group read: members, held roles and site; empty-member retry loop removed
- Site grants in bag phases: users, user groups, admin accounts
- StaticEntitlements + SkipEntitlements on group, userGroup, site and role; role descriptions on the resource
- Grant pages capped at 1000 with a keyset cursor
- Session store: privilege catalog read once; username/email -> user id written by user List, read by device Grants
- Remove getUserIndex, the in-process caches and the per-item Classic detail reads
- Device Grant resolves the replaced grant through a Pro users RSQL lookup
- Pro API 404 -> FailedPrecondition (users, accounts, site objects, privileges)
- userAccount display name uses FullName
- CreateAccount/Delete input errors -> InvalidArgument; Delete refuses the connector's own account
- Enable the session store; move resource types to resource_types.go
- Mock: Pro users, accounts and site objects endpoints; README sync-cost section; openapi spec; capabilities regenerated
- Tests for grant paging, role/site/group grant phases, device owner lookup, Pro client paging and 404 mapping
@mateovespConductor
mateovespConductor force-pushed the mateovesp/cxp-1240-sync-cost branch from 491af91 to 2e632ac Compare October 30, 2026 20:33
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 2e632aca3b49

Connector PR Review: [CXP-1240] Reduce and improve the cost and speed of synchronization

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: full
View review run

Review Summary

The PR moves user and admin-account listing onto the paged Jamf Pro API (/api/v1/users, /api/v1/accounts). Site grants now come from /api/v1/sites/{id}/objects and the accounts list filtered by siteId. Role grants are type-scoped: privilege sets come from the accounts list, and Custom accounts are read 10 per page. Group role and site grants are emitted from group Grants. group/userGroup/site/role use StaticEntitlements with keyset-paged grants, and the privilege catalog and the username/email → user-id index live in the session store (WithSessionStoreEnabled).

I scanned the full PR diff for security and correctness and read every production file in pkg/connector and pkg/jamf. I read tests, test-server/ and specs/openapi.json only lightly. Trusted criteria were applied as follows:

  • S1/S5/S6: the only per-item skip is the 404 in customPrivilegeGrants, and the doc comment on GetUserAccountDetails states that a 404 means absence. Pro-endpoint 404s become FailedPrecondition, so they fail the sync instead of returning empty.
  • L levels: new log lines are Debug.
  • E2: no hand-wrapped 429/5xx.
  • C/D: no spans; id,string IDs.
  • B/BP: resource, entitlement and grant IDs are unchanged.
  • PR1/PR2: Grant/Revoke entity sources are unchanged.

I could not verify Jamf's live behavior: the page-size caps, whether the RSQL privilegeLevel/siteId filters are supported, and the empty-members bug.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence medium), pkg/connector/group.go:110-119: the retry for an empty members read was removed. README.md:54-59 still says Jamf can transiently return an empty member list for a populated group, and Grant/Revoke refuse to trust such a read. In sync, one transient empty read emits zero member grants, so C1 revokes every member.
  • New (confidence medium), pkg/jamf/client.go:362: GetSiteObjects stops when a page holds fewer than 1000 objects, but Jamf documents no maximum page-size for that endpoint. If Jamf caps it lower, only the first page of site members is emitted and the rest have their site grants revoked.
  • Prior — still present (confidence high), pkg/jamf/client.go:37: user sync now needs /api/v1/users (Jamf Pro ≥ 11.26 per the PR body) with no Classic fallback. Older instances will fail every sync with FailedPrecondition. The PR body says the README documents this, but neither README.md nor docs/connector.mdx mentions 11.26.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/group.go`:
- Around line 110-119: groupGrants emits member grants straight from one GetGroupDetails read, so a transient empty `members` list emits zero member grants and C1 revokes every member. README.md:54-59 says Jamf can do this. Re-read the group (fresh, with uhttp.WithNoCache via jamf.WithFreshReads) when Members is empty, and keep the second result. Alternatively, if the bug is truly unreproducible, update README.md:54-59 so the docs and the sync path agree.

In `pkg/jamf/client.go`:
- Around line 361-365: GetSiteObjects stops paging when len(objects) != SiteObjectsPageSize (1000), but Jamf documents no maximum page-size for /api/v1/sites/{id}/objects. A server-side cap below 1000 would truncate site membership after the first page. Return a next-page token whenever the page is non-empty and stop only on an empty page; update the tests to match.
- Around line 37: /api/v1/users needs Jamf Pro >= 11.26 and there is no Classic fallback. Add this minimum-version requirement to README.md and docs/connector.mdx, since the PR body says the README covers it.

Comment thread pkg/jamf/client.go Resolved
Comment thread pkg/connector/group.go Resolved
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit fe05279c7356

Connector PR Review: [CXP-1240] Reduce and improve the cost and speed of synchronization

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: full
View review run

Review Summary

The PR moves user and admin-account listing onto the paged Jamf Pro API (/api/v1/users, /api/v1/accounts). Site grants now come from /api/v1/sites/{id}/objects and the accounts list filtered by siteId. Role grants are type-scoped: privilege sets come from the accounts list, and Custom accounts are read 10 per page. Group role and site grants are emitted from group Grants. group/userGroup/site/role use StaticEntitlements with keyset-paged grants, and the privilege catalog and the username/email → user-id index live in the session store (WithSessionStoreEnabled).

I scanned the full PR diff for security and correctness and read every production file in pkg/connector and pkg/jamf. I read tests, test-server/ and specs/openapi.json only lightly. Trusted criteria were applied as follows:

  • S1/S5/S6: the only per-item skip is the 404 in customPrivilegeGrants, and the doc comment on GetUserAccountDetails states that a 404 means absence. Pro-endpoint 404s become FailedPrecondition, so they fail the sync instead of returning empty.
  • L levels: new log lines are Debug.
  • E2: no hand-wrapped 429/5xx.
  • C/D: no spans; id,string IDs.
  • B/BP: resource, entitlement and grant IDs are unchanged.
  • PR1/PR2: Grant/Revoke entity sources are unchanged.

I could not verify Jamf's live behavior: the page-size caps, whether the RSQL privilegeLevel/siteId filters are supported, and the empty-members bug.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence medium), pkg/connector/group.go:110-119: the retry for an empty members read was removed. README.md:54-59 still says Jamf can transiently return an empty member list for a populated group, and Grant/Revoke refuse to trust such a read. In sync, one transient empty read emits zero member grants, so C1 revokes every member.
  • New (confidence medium), pkg/jamf/client.go:362: GetSiteObjects stops when a page holds fewer than 1000 objects, but Jamf documents no maximum page-size for that endpoint. If Jamf caps it lower, only the first page of site members is emitted and the rest have their site grants revoked.
  • Prior — still present (confidence high), pkg/jamf/client.go:37: user sync now needs /api/v1/users (Jamf Pro ≥ 11.26 per the PR body) with no Classic fallback. Older instances will fail every sync with FailedPrecondition. The PR body says the README documents this, but neither README.md nor docs/connector.mdx mentions 11.26.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/group.go`:
- Around line 110-119: groupGrants emits member grants straight from one GetGroupDetails read, so a transient empty `members` list emits zero member grants and C1 revokes every member. README.md:54-59 says Jamf can do this. Re-read the group (fresh, with uhttp.WithNoCache via jamf.WithFreshReads) when Members is empty, and keep the second result. Alternatively, if the bug is truly unreproducible, update README.md:54-59 so the docs and the sync path agree.

In `pkg/jamf/client.go`:
- Around line 361-365: GetSiteObjects stops paging when len(objects) != SiteObjectsPageSize (1000), but Jamf documents no maximum page-size for /api/v1/sites/{id}/objects. A server-side cap below 1000 would truncate site membership after the first page. Return a next-page token whenever the page is non-empty and stop only on an empty page; update the tests to match.
- Around line 37: /api/v1/users needs Jamf Pro >= 11.26 and there is no Classic fallback. Add this minimum-version requirement to README.md and docs/connector.mdx, since the PR body says the README covers it.

Reviewed commit: 2e632aca3b49

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

@FeliLucero1 FeliLucero1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Big improvement overall. IDs, slugs and grant IDs all match v0.2.4, and the keyset paging, TypeScopedGrants and session store wiring look right. A few things inline, and +1 on two of the bot's threads.

One thing from the ticket that isn't touched: the customer's "users named user". The user display name is still just user.FullName (user.go:60) with no fallback, so anyone without a real name gets an empty display name. I can't prove that's what they're seeing, but it's the obvious suspect. Maybe fall back to FullName, then Username, then Email?

Also FYI, c1 still runs Jamf in-process at v0.0.9 with no session store wired, so none of this reaches hosted customers yet, and a straight bump would hit the session-store paths.

Comment thread pkg/connector/userGroup.go Resolved
Comment thread pkg/connector/group.go Resolved
syncRoles: opts.WillSyncResourceType(resourceTypeRole.Id),
syncSites: opts.WillSyncResourceType(resourceTypeSite.Id),
syncUserAccounts: opts.WillSyncResourceType(resourceTypeUserAccount.Id),
syncManagedDevices: opts.WillSyncResourceType(resourceTypeManagedDevice.Id),

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.

small one: WillSyncResourceType returns true when there's no type filter, so user List fills the username/email cache (about 40 session RPCs per 1000 users) even when devices aren't being synced. Not a bug, just extra work.

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.

With no filter every type syncs, devices included, so the cache is needed. With selective sync, c1 sends an explicit list and the cache is skipped when devices are off.

…ssion store (1000 member ids per key)

- User display name falls back to the username, then the email (CXP-1286)
- README, connector.mdx and docs-info state the Jamf Pro minimum versions
- README sync cost: one read per user group
- Tests: user group paging with one Jamf read and a store miss; group tests reuse groupPrincipal
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit fe05279c7356

Connector PR Review: [CXP-1240] Reduce and improve the cost and speed of synchronization

Blocking Issues: 0 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: incremental since 2e632ac
View review run

Review Summary

The new commit changes how userGroup Grants pages through members. It reads the group from Jamf once, splits the sorted member ids into pages of 1000, and stores the later pages in the session store under user_group_members:<id>:<page>. If a later page is missing from the store, it reads the group from Jamf again. The commit also adds a display-name fallback for users (full name, then username, then email) and documents the Jamf Pro 11.26 minimum in the README, connector.mdx and docs-info.md. Tests now cover a 1500-member group read once from Jamf and a page-1 read when the store misses.

I scanned the full PR diff for security and correctness issues. I checked the SDK session helpers in vendor/.../pkg/session. SetManyJSON skips an empty map (the single-page case) and splits large writes, so the new caching is safe. I applied the repo-local criteria to the new code:

  • S1/S5: a Jamf read error is returned, never turned into an empty page. An out-of-range page returns an empty Last page only after a successful read.
  • L: the new code adds no log lines.
  • R8/F2: paging is driven by the SDK page token, one page per call.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence medium), pkg/connector/group.go:108-118: groupGrants builds member grants from a single GetGroupDetails read with no retry when members comes back empty. README.md:54-59 still says Jamf can transiently return an empty member list for a populated group, and Grant/Revoke refuse to trust such a read. In sync, one such read emits zero member grants, and C1 revokes every member.
  • Prior — still present (confidence medium), pkg/jamf/client.go:361-365: GetSiteObjects stops paging when a page holds fewer than SiteObjectsPageSize objects. If Jamf caps page-size below that value, only the first page of site members is emitted, and the rest lose their site grants.

Resolved prior findings

  • The missing Jamf Pro 11.26 requirement is fixed. README.md:18, docs/connector.mdx:40 and docs/docs-info.md:75 now state the 11.26.0 minimum for /api/v1/users, and docs-info.md notes that older instances fail with FailedPrecondition.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/group.go`:
- Around line 108-118: groupGrants emits member grants straight from one GetGroupDetails read, so a transient empty `members` list emits zero member grants and C1 revokes every member. README.md:54-59 says Jamf can do this. When group.Members is empty, re-read the group with a fresh, uncached read (jamf.WithFreshReads(ctx)) and use the second result. Alternatively, if the transient empty read cannot be reproduced, update README.md:54-59 so the docs and the sync path agree.

In `pkg/jamf/client.go`:
- Around line 361-365: GetSiteObjects treats a short page (fewer than SiteObjectsPageSize objects) as the last page. Confirm Jamf honors that page-size on GET /api/v1/sites/{id}/objects. If Jamf caps the page size lower, compute the next page from the returned count or a documented total, or lower SiteObjectsPageSize to the documented maximum.

@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 5164e55709a9

Connector PR Review: [CXP-1240] Reduce and improve the cost and speed of synchronization

Blocking Issues: 0 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: incremental since 2e632ac
View review run

Review Summary

The new commit changes how userGroup Grants pages through members. It reads the group from Jamf once, splits the sorted member ids into pages of 1000, and stores the later pages in the session store under user_group_members:<id>:<page>. If a later page is missing from the store, it reads the group from Jamf again. The commit also adds a display-name fallback for users (full name, then username, then email) and documents the Jamf Pro 11.26 minimum in the README, connector.mdx and docs-info.md. Tests now cover a 1500-member group read once from Jamf and a page-1 read when the store misses.

I scanned the full PR diff for security and correctness issues. I checked the SDK session helpers in vendor/.../pkg/session. SetManyJSON skips an empty map (the single-page case) and splits large writes, so the new caching is safe. I applied the repo-local criteria to the new code:

  • S1/S5: a Jamf read error is returned, never turned into an empty page. An out-of-range page returns an empty Last page only after a successful read.
  • L: the new code adds no log lines.
  • R8/F2: paging is driven by the SDK page token, one page per call.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence medium), pkg/connector/group.go:108-118: groupGrants builds member grants from a single GetGroupDetails read with no retry when members comes back empty. README.md:54-59 still says Jamf can transiently return an empty member list for a populated group, and Grant/Revoke refuse to trust such a read. In sync, one such read emits zero member grants, and C1 revokes every member.
  • Prior — still present (confidence medium), pkg/jamf/client.go:361-365: GetSiteObjects stops paging when a page holds fewer than SiteObjectsPageSize objects. If Jamf caps page-size below that value, only the first page of site members is emitted, and the rest lose their site grants.

Resolved prior findings

  • The missing Jamf Pro 11.26 requirement is fixed. README.md:18, docs/connector.mdx:40 and docs/docs-info.md:75 now state the 11.26.0 minimum for /api/v1/users, and docs-info.md notes that older instances fail with FailedPrecondition.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/group.go`:
- Around line 108-118: groupGrants emits member grants straight from one GetGroupDetails read, so a transient empty `members` list emits zero member grants and C1 revokes every member. README.md:54-59 says Jamf can do this. When group.Members is empty, re-read the group with a fresh, uncached read (jamf.WithFreshReads(ctx)) and use the second result. Alternatively, if the transient empty read cannot be reproduced, update README.md:54-59 so the docs and the sync path agree.

In `pkg/jamf/client.go`:
- Around line 361-365: GetSiteObjects treats a short page (fewer than SiteObjectsPageSize objects) as the last page. Confirm Jamf honors that page-size on GET /api/v1/sites/{id}/objects. If Jamf caps the page size lower, compute the next page from the returned count or a documented total, or lower SiteObjectsPageSize to the documented maximum.

Reviewed commit: fe05279c7356

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

@mateovespConductor mateovespConductor changed the title [CXP-1240] Reduce and improve the cost and speed of synchronization [CXP-1240][CXP-1286] Reduce and improve the cost and speed of synchronization, and fix users shown as "user" Nov 2, 2026

@FeliLucero1 FeliLucero1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, the user group paging, the display-name fallback and the version docs all look good. I checked the versions against the Jamf changelogs and they match. One small thing inline.

Comment thread pkg/connector/userGroup.go Outdated
if err != nil {
return userGroupMemberPage{}, err
}
pages := memberPages(group.Users)

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.

edge case: after a store miss on page k, this re-reads the group and returns pages[k] from the new list, but pages 0..k-1 came from the old read. If someone with a lower id left in between, everything shifts one slot and the member at the page boundary gets skipped. If someone joined, one member comes out twice. It's rare (needs a miss plus a membership change mid-sync), but carrying the last emitted user id in the token and filtering id > cursor on a re-read would close it, the way pageGrants did.

Comment thread pkg/connector/site.go Resolved
Comment thread pkg/jamf/client.go Resolved
Comment thread pkg/connector/group.go Resolved
Comment thread docs/connector.mdx Resolved
…-read after a store miss keeps only the ids above it

- Site grants only read the users, user groups and admin accounts the sync includes
- Every client method returns Jamf's rate-limit annotations; List and Grants pass them to the SDK, and Grant/Revoke/CreateAccount/Delete return the write's
- Docs: empty-member guard reworded; Delete refuses the connector's own account; site sync cost per synced type
- Tests: user group store miss after a member leaves; site sync without userAccount, or without any of its principal types, reads nothing it doesn't need
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 5164e55709a9

Connector PR Review: [CXP-1240][CXP-1286] Reduce and improve the cost and speed of synchronization, and fix users shown as "user"

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: incremental since fe05279
View review run

Review Summary

The new commit makes three code changes:

  • User group member pages. Pages are now keyed in the session store by the last emitted user id. A store miss re-reads the group and keeps only ids above the cursor, so pages are neither duplicated nor skipped, and an empty remainder returns Last (userGroup.go:117-160, session_cache.go:89-106).
  • Site grants. Grants now read only the principal phases the sync includes, using WillSyncResourceType. When none of those types is synced, the method returns an empty grant set (site.go:98-114, connector.go:83-87). Nothing can be lost there, because no matching principals are synced.
  • Rate limits. Rate-limit annotations now flow from doRequestWithMethodAndBodyEncoding through every client method. List, Grants, Grant, Revoke, CreateAccount and Delete return them. GrantAlreadyExists and GrantAlreadyRevoked are now merged into the read's annotations instead of replacing them.

The commit also rewords docs and adds tests. The full PR diff was scanned for security and correctness.

Repo-local criteria applied:

  • S1, S5, S6 (empty-result data loss): No new swallowed errors or empty returns. The one NotFound skip in customPrivilegeGrants is backed by the 404-means-absent doc comment on GetUserAccountDetails (client.go:191).
  • L1–L7 (log levels): No new log lines.
  • E1–E3 (error wrapping): Error wrapping is unchanged.
  • Provisioning (PR1–PR3, P1–P6): Grant and Revoke still take ids from principal.Id and entitlement.Resource.Id. Only the annotations changed.
  • B1–B10 (breaking changes): No ids or endpoints changed.

There are no gaps in coverage: the incremental artifact is not partial and no paths were dropped.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence low), pkg/jamf/client.go:375-379: GetSiteObjects decides there is another page only when a page holds exactly SiteObjectsPageSize (1000) objects, because the response has no total count. If Jamf silently capped page-size below 1000, paging would stop after the first page, and later site members would lose their site grants. The code comment says Jamf documents no maximum, which lowers the risk.

Resolved prior findings

  • Fixed (docs alternative): group.go builds member grants from a single read. The docs no longer claim Jamf transiently returns empty member lists. README.md:54-59 now presents an incomplete read as a hypothetical that Grant/Revoke guard against, and group.go:160-164 says the case could not be reproduced. Docs and sync path now agree, which was the alternative the prior finding offered.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/jamf/client.go`:
- Around line 375-379: GetSiteObjects only requests another page when a page holds exactly SiteObjectsPageSize (1000) objects. If Jamf caps page-size below 1000, paging stops after page 1 and site members are dropped. Either confirm against a real tenant that Jamf honors page-size=1000 for /api/v1/sites/{id}/objects and note it in the doc comment, or keep paging until a page comes back empty (or shorter than the size Jamf actually applied).

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit d860368a1252

Connector PR Review: [CXP-1240][CXP-1286] Reduce and improve the cost and speed of synchronization, and fix users shown as "user"

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: incremental since fe05279
View review run

Review Summary

The new commit makes three code changes:

  • User group member pages. Pages are now keyed in the session store by the last emitted user id. A store miss re-reads the group and keeps only ids above the cursor, so pages are neither duplicated nor skipped, and an empty remainder returns Last (userGroup.go:117-160, session_cache.go:89-106).
  • Site grants. Grants now read only the principal phases the sync includes, using WillSyncResourceType. When none of those types is synced, the method returns an empty grant set (site.go:98-114, connector.go:83-87). Nothing can be lost there, because no matching principals are synced.
  • Rate limits. Rate-limit annotations now flow from doRequestWithMethodAndBodyEncoding through every client method. List, Grants, Grant, Revoke, CreateAccount and Delete return them. GrantAlreadyExists and GrantAlreadyRevoked are now merged into the read's annotations instead of replacing them.

The commit also rewords docs and adds tests. The full PR diff was scanned for security and correctness.

Repo-local criteria applied:

  • S1, S5, S6 (empty-result data loss): No new swallowed errors or empty returns. The one NotFound skip in customPrivilegeGrants is backed by the 404-means-absent doc comment on GetUserAccountDetails (client.go:191).
  • L1–L7 (log levels): No new log lines.
  • E1–E3 (error wrapping): Error wrapping is unchanged.
  • Provisioning (PR1–PR3, P1–P6): Grant and Revoke still take ids from principal.Id and entitlement.Resource.Id. Only the annotations changed.
  • B1–B10 (breaking changes): No ids or endpoints changed.

There are no gaps in coverage: the incremental artifact is not partial and no paths were dropped.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence low), pkg/jamf/client.go:375-379: GetSiteObjects decides there is another page only when a page holds exactly SiteObjectsPageSize (1000) objects, because the response has no total count. If Jamf silently capped page-size below 1000, paging would stop after the first page, and later site members would lose their site grants. The code comment says Jamf documents no maximum, which lowers the risk.

Resolved prior findings

  • Fixed (docs alternative): group.go builds member grants from a single read. The docs no longer claim Jamf transiently returns empty member lists. README.md:54-59 now presents an incomplete read as a hypothetical that Grant/Revoke guard against, and group.go:160-164 says the case could not be reproduced. Docs and sync path now agree, which was the alternative the prior finding offered.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/jamf/client.go`:
- Around line 375-379: GetSiteObjects only requests another page when a page holds exactly SiteObjectsPageSize (1000) objects. If Jamf caps page-size below 1000, paging stops after page 1 and site members are dropped. Either confirm against a real tenant that Jamf honors page-size=1000 for /api/v1/sites/{id}/objects and note it in the doc comment, or keep paging until a page comes back empty (or shorter than the size Jamf actually applied).

Reviewed commit: 5164e55709a9

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

Comment thread pkg/connector/group.go
// if this endpoint becomes reliable again, we can remove this for loop
var group *jamf.Group
count := 0
for count < 5 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why was this removed? If the previous code comment is right, then we probably should keep something that help us avoid this "returns empty without failing" situation.
If the comment wasn't right, then I agree on removing it

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.

The comment was true when it was written: #14 has a video of it happening in May 2025. Today I couldn't reproduce it on the trial: about 4,200 reads with both auth types, in parallel and right after membership changes, and the members came back every time. The loop also wasn't protecting us. Its retries got the cached response back, and after 5 empty reads it carried on with no members anyway. Grant/Revoke still refuse to write over an empty read, which is where a false empty would do real damage

Comment thread pkg/connector/helpers.go

nextCursor := page[len(page)-1].GetId()
if nextCursor == "" {
return nil, "", errors.New("jamf-connector: grants page has more items but an empty cursor")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: new errors in this PR use the legacy jamf-connector: prefix (here and in group, role, site, userGroup, userAccount). CLAUDE.md asks for baton-jamf:; consider switching the new ones.

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.

Thanks! I'll switch all of them in a follow-up PR, together with renaming the files from camelCase to snake_case, so this one stays focused and the prefix doesn't end up mixed.

Comment thread pkg/connector/group.go Outdated
}

return rv, nil, nil
page, nextCursor, err := pageGrants(grants, attrs.PageToken.Token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: each grants page calls groupGrants again, so every page re-reads GetGroupDetails (and the privilege catalog for Custom groups). Consider storing later pages in the session store, as userGroup does with cacheUserGroupMemberPages/memberPage.

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 in d860368 thanks!

Comment thread pkg/connector/role.go Outdated
}
var held []string
for _, privilege := range dedupeStrings(privileges.All()) {
if slices.Contains(catalog, privilege) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: slices.Contains over the full catalog makes this O(n·m) per Custom account/group. Consider building a map[string]struct{} from the catalog once (e.g. in privilegeCatalog) and checking against that.

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 in d860368 thanks!

"user_id": fmt.Sprintf("account:%d", account.ID),
}

displayName := account.FullName

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: the display name now uses FullName, but first_name/last_name still come from SplitFullName(account.Name) (the username). Use the same FullName → Name fallback for both.

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.

I kept first_name/last_name as they shipped, since changing a published profile value can break customer CEL and attribute mappings that read them. The real name is now in a new full_name key, the same one user profiles have, and the display name already uses it

…-read after a store miss keeps only the ids above it

- Group grants read the group once; a group with more than one page is stored for its later pages
- Site grants only read the users, user groups and admin accounts the sync includes
- Every client method returns Jamf's rate-limit annotations; List and Grants pass them to the SDK, and Grant/Revoke/CreateAccount/Delete return the write's
- Custom privileges are checked against a set built from the catalog
- Admin account profiles gain full_name; first and last name keep their shipped values
- Docs: empty-member guard reworded; Delete refuses the connector's own account; group and site sync cost
- Tests: user group and group store misses; site sync without userAccount, or without any of its principal types, reads nothing it doesn't need
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit d860368a1252

Connector PR Review: [CXP-1240][CXP-1286] Reduce and improve the cost and speed of synchronization, and fix users shown as "user"

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: incremental since 5164e55
View review run

Review Summary

The new commits change how group Grants page:

  • Group Grants. The first call reads the group once. If the group has more than one page, its jamf.Group detail goes into the session store under group_details: (session_cache.go:123-131), and later pages are built from the stored copy. If the store misses, the group is read again. The keyset grant-id cursor in pageGrants (helpers.go:22-41) means the re-read path neither duplicates nor skips grants (group.go:104-131).
  • Privilege catalog. Custom privileges are now checked against a map built from the catalog (privilegeCatalogSet).
  • Admin account profiles. They gain full_name; first_name and last_name are unchanged.
  • README. The group request-cost row is updated.

Coverage and criteria:

  • I scanned the full 38-file PR diff for security and correctness. The incremental artifact was complete (partial: false).
  • Group, BaseType and Privileges carry JSON tags on every field, so the stored detail round-trips without loss.
  • The trusted repo-local criteria were applied as follows:
    • S1/S5/S6 (empty-result data loss): No new swallowed errors or empty returns. Store errors and read errors are returned from Grants.
    • R8/F2 (pagination): Each call returns one page, and the SDK drives the next.
    • J1–J3, A (JSON types, log levels): Nothing applicable changed.
    • Provisioning: Grant and Revoke are untouched in this increment.
  • The new test covers both the store hit (1 Jamf read across 2 pages) and the store-miss re-read.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence low), pkg/jamf/client.go:375-379: GetSiteObjects only treats a page holding exactly SiteObjectsPageSize (1000) objects as having a next page, because the response has no total count. If Jamf ever capped page-size below 1000, paging would stop after page 1 and later site members would lose their site grants. The code comment says Jamf documents no maximum, which lowers the risk.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/jamf/client.go`:
- Around line 375-379: GetSiteObjects decides there is another page only when len(objects) == SiteObjectsPageSize. If Jamf silently caps page-size below the requested 1000, the loop stops after page 1. Make it robust: keep requesting the next page while the page is non-empty, and stop on an empty page. Alternatively, confirm the server-side maximum page size and document it next to the constant.

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Connector PR Review: [CXP-1240][CXP-1286] Reduce and improve the cost and speed of synchronization, and fix users shown as "user"

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 43c585b6471e.
Review mode: incremental since 5164e55
View review run

Review Summary

The new commits change how group Grants page:

  • Group Grants. The first call reads the group once. If the group has more than one page, its jamf.Group detail goes into the session store under group_details: (session_cache.go:123-131), and later pages are built from the stored copy. If the store misses, the group is read again. The keyset grant-id cursor in pageGrants (helpers.go:22-41) means the re-read path neither duplicates nor skips grants (group.go:104-131).
  • Privilege catalog. Custom privileges are now checked against a map built from the catalog (privilegeCatalogSet).
  • Admin account profiles. They gain full_name; first_name and last_name are unchanged.
  • README. The group request-cost row is updated.

Coverage and criteria:

  • I scanned the full 38-file PR diff for security and correctness. The incremental artifact was complete (partial: false).
  • Group, BaseType and Privileges carry JSON tags on every field, so the stored detail round-trips without loss.
  • The trusted repo-local criteria were applied as follows:
    • S1/S5/S6 (empty-result data loss): No new swallowed errors or empty returns. Store errors and read errors are returned from Grants.
    • R8/F2 (pagination): Each call returns one page, and the SDK drives the next.
    • J1–J3, A (JSON types, log levels): Nothing applicable changed.
    • Provisioning: Grant and Revoke are untouched in this increment.
  • The new test covers both the store hit (1 Jamf read across 2 pages) and the store-miss re-read.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence low), pkg/jamf/client.go:375-379: GetSiteObjects only treats a page holding exactly SiteObjectsPageSize (1000) objects as having a next page, because the response has no total count. If Jamf ever capped page-size below 1000, paging would stop after page 1 and later site members would lose their site grants. The code comment says Jamf documents no maximum, which lowers the risk.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/jamf/client.go`:
- Around line 375-379: GetSiteObjects decides there is another page only when len(objects) == SiteObjectsPageSize. If Jamf silently caps page-size below the requested 1000, the loop stops after page 1. Make it robust: keep requesting the next page while the page is non-empty, and stop on an empty page. Alternatively, confirm the server-side maximum page size and document it next to the constant.

Reviewed commit: d860368a1252

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

Comment thread pkg/connector/group.go
}

// The group read supplies the annotations.
catalog, err := privilegeCatalogSet(ctx, store, g.client)

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.

[Connector] Privilege catalog read drops its rate-limit annotations

For a Custom group, heldRoles reads the privilege catalog after the group, but privilegeCatalogSet discards that call's annotations (session_cache.go:53). When the catalog isn't in the store yet, the page reports the group read's rate-limit state rather than the most recent call's.

Comment thread pkg/jamf/client.go
}
response, err := c.wrapper.Do(request)
var rateLimit v2.RateLimitDescription
response, err := c.wrapper.Do(request, uhttp.WithRatelimitData(&rateLimit))

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.

[Client] Jamf error bodies aren't parsed

Do is called without uhttp.WithErrorResponse, so when the new Pro API endpoints (/api/v1/users, /api/v1/accounts, /api/v1/sites/{id}/objects, /api/v1/api-role-privileges) fail, Jamf's error message doesn't reach the returned error. Only 401 and 404 get a hand-written message (here and in errors.go).

Comment thread docs/connector.mdx
- Jamf has two distinct account types: **Users** (directory users) and **User Accounts** (Jamf Pro console admins). The connector can only create **one** of these types per connector instance — set by the **Account Provisioning Target** configuration field. Deletion works for both types regardless of this setting. Created accounts are `Enabled`, `Full Access`, and default to the `Auditor` privilege set; a random password is generated and returned once, as plaintext. See [Jamf account privileges](#jamf-account-privileges) below for the privileges this requires.
- Jamf has two distinct account types: **Users** (directory users) and **User Accounts** (Jamf Pro console admins). The connector can only create **one** of these types per connector instance — set by the **Account Provisioning Target** configuration field. Deletion works for both types regardless of this setting, except for the user account the connector authenticates with, which Delete refuses. Created accounts are `Enabled`, `Full Access`, and default to the `Auditor` privilege set; a random password is generated and returned once, as plaintext. See [Jamf account privileges](#jamf-account-privileges) below for the privileges this requires.
- **User Groups**: Grant/Revoke adds or removes a Jamf user from a **static** User Group only — Smart User Groups are rejected.
- **Sites**: Grant/Revoke is only supported for the `user` principal — a Jamf user's site membership is a true multi-valued list. `userGroup`/`userAccount`/`group` site membership remains sync-only (each of those objects can only belong to one site at a time, so granting a new one would silently displace the old one instead of adding a membership).

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] Notes explain internal mechanics

A few parentheticals in these notes explain how Jamf works rather than what the admin can do:

  • here: "(each of those objects can only belong to one site at a time, so granting a new one would silently displace the old one…)"
  • L33: "(an ambiguous empty read is never written over, or assumed to mean someone was removed)"
  • L34: "(Jamf's lowest access, and not itself revocable)" and "(that membership granted it nothing)"

Comment thread docs/connector.mdx
</Note>

Tested with Jamf Pro 11.32.1. Managed Devices use the Jamf Pro API v4 computers-inventory and v2 mobile-devices endpoints.
Requires Jamf Pro 11.26.0 or later (Managed Devices: 11.30.0). Tested with Jamf Pro 11.32.1.

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] Test evidence in the docs

"Tested with Jamf Pro 11.32.1" is test evidence, not an instruction, and it goes stale with the next Jamf release. Same sentence in README.md:18.

Comment thread docs/connector.mdx
`Read Accounts`/`Create Accounts`/`Update Accounts`/`Delete Accounts` cover both admin user accounts and admin groups — Jamf has no separate privilege for account groups. Every write path reads the object first, so each `Update` privilege above needs its `Read` counterpart too.

Jamf reports a missing privilege differently depending on which API it uses: the Classic API (`/JSSResource` — Users, Accounts, Static User Groups) answers with HTTP 401, and the Jamf Pro API (`/api/v1`, `/api/v2`, `/api/v4` — the roles catalog, Computers, Mobile Devices) with HTTP 403. The connector reports both as a permission error.
Jamf reports a missing privilege differently depending on which API it uses: the Classic API (`/JSSResource` — User Groups, admin groups, Sites, and user and account details and writes) answers with HTTP 401, and the Jamf Pro API (`/api/v1`, `/api/v2`, `/api/v4` — the users and accounts lists, site objects, the roles catalog, Computers, Mobile Devices) with HTTP 403. The connector reports both as a permission error.

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] 401 vs 403 paragraph is implementation detail

Which API returns 401 and which returns 403, with the /JSSResource and /api/v1 paths, is internal behavior. An admin only needs to know that a missing privilege shows up as a permission error.

Comment thread README.md
list even when the group genuinely has members, and the connector refuses to
write over (or assume someone was removed from) a list it can't trust is
complete. Only Group Access
instead of reporting success. The write replaces the whole member list, so 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.

[Docs] Reasoning instead of behavior

"The write replaces the whole member list, so the connector never writes over an empty read… if that read were incomplete, the write would drop every real member" explains why the guard exists. The behavior (first member can't be added, Revoke returns a retryable error) is already stated in the lines above.

Comment thread docs/docs-info.md
- Jamf Pro API: `POST /api/v1/auth/token` or `POST /api/v1/oauth/token` (authentication), `GET /api/v1/auth` (validation), `GET /api/v1/api-role-privileges` (Role sync), `GET /api/v4/computers-inventory` and `PATCH /api/v4/computers-inventory-detail/{id}` (Managed Device computers), `GET`/`PATCH /api/v2/mobile-devices[/{id}]` (Managed Device mobile devices).
- Jamf Pro API: `POST /api/v1/auth/token` or `POST /api/v1/oauth/token` (authentication), `GET /api/v1/auth` (validation and the delete self-check), `GET /api/v1/api-role-privileges` (Role sync), `GET /api/v1/users` (User sync and managed device owner lookup), `GET /api/v1/accounts` (User Account sync, Role and Site grants), `GET /api/v1/sites/{id}/objects` (Site grants), `GET /api/v4/computers-inventory` and `PATCH /api/v4/computers-inventory-detail/{id}` (Managed Device computers), `GET`/`PATCH /api/v2/mobile-devices[/{id}]` (Managed Device mobile devices).

Version requirements: Jamf Pro 11.26.0 or later. `GET /api/v1/users` was added in 11.26.0, `GET /api/v1/accounts` in 11.20.0, `GET /api/v1/sites/{id}/objects` in 11.2.0 and `GET /api/v1/api-role-privileges` in 10.49.0; an older instance fails the sync with `FailedPrecondition` naming the missing endpoint. Managed Devices (opt-in) need 11.30.0 for `/api/v4/computers-inventory`.

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] Per-endpoint minimum versions aren't verifiable from the code

/api/v1/accounts in 11.20.0, /api/v1/sites/{id}/objects in 11.2.0 and /api/v1/api-role-privileges in 10.49.0 can't be checked against anything in the repo. What's the source for these (Jamf release notes, API changelog)?

Comment thread docs/connector.mdx

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] "ConductorOne" instead of "C1"

The docs prose uses "C1", but this file still says "ConductorOne" in the frontmatter description and og:description (L4-5), the Account Provisioning Target field (L163, "Which Jamf account type ConductorOne should create") and the download link text (L312, "ConductorOne download center"). Not changed by this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants