Skip to content

Guard workspace_role grant emission behind WillSyncResourceType - #98

Merged
laurenleach merged 3 commits into
mainfrom
lauren/guard-workspace-role-grant-emission
Aug 14, 2026
Merged

Guard workspace_role grant emission behind WillSyncResourceType#98
laurenleach merged 3 commits into
mainfrom
lauren/guard-workspace-role-grant-emission

Conversation

@laurenleach

Copy link
Copy Markdown
Contributor

Gates cross-type grant emission from the user syncer on the customer's sync
filter, so grants aren't emitted for a resource type the sync excludes.
Reference: ConductorOne/baton-linear#55.

Each target is guarded individually in Grants(); when every target is excluded
the user resource type is annotated SkipEntitlementsAndGrants so the SDK skips
the pass entirely.

Flags are named skipResourceType and stored inverted so the zero value
means "sync everything" — main.go registers a zero-value Connector{} as the
capabilities factory, bypassing New.

Build, tests, and golangci-lint (0 issues) pass.

No resource-type annotation here, deliberately. workspaceResourceType has
its own member entitlement (grantable to user), so SkipEntitlements /
SkipEntitlementsAndGrants would suppress real data. Every grant the workspace
syncer emits targets workspace_role, so Grants returns early when that type
is filtered out — which also skips paging through workspace users.

Gates cross-type grant emission from the user syncer on the customer's sync
filter, so grants aren't emitted for a resource type the sync excludes.
Reference: ConductorOne/baton-linear#55.

Each target is guarded individually in Grants(); when every target is excluded
the user resource type is annotated SkipEntitlementsAndGrants so the SDK skips
the pass entirely.

Flags are named skip<Type>ResourceType and stored inverted so the zero value
means "sync everything" — main.go registers a zero-value Connector{} as the
capabilities factory, bypassing New.

Build, tests, and golangci-lint (0 issues) pass.

**No resource-type annotation here, deliberately.** `workspaceResourceType` has
its own `member` entitlement (grantable to user), so `SkipEntitlements` /
`SkipEntitlementsAndGrants` would suppress real data. Every grant the workspace
syncer emits targets `workspace_role`, so `Grants` returns early when that type
is filtered out — which also skips paging through workspace users.
Comment thread pkg/connector/workspace.go Outdated
Comment on lines +151 to +155
// Every grant below targets workspace_role; skip the user pagination
// entirely when that type isn't part of the sync.
if o.skipWorkspaceRoleResourceType {
return nil, &resources.SyncOpResults{}, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 Bug: The premise "every grant below targets workspace_role" isn't true — line 287 emits grant.NewGrant(resource, memberEntitlement, userID), a grant on the workspace's own member entitlement (the function's doc comment on line 145 even says "sets workspace memberships and workspace roles"). This is the only place workspace member grants are produced, so filtering workspace_role out of a sync now silently drops every workspace membership grant, which downstream reads as mass revocation. This is the same suppression the PR deliberately avoided by not using SkipEntitlementsAndGrants.

Suggested fix: keep the user pagination and gate only the roleResource(...) grant appends on !o.skipWorkspaceRoleResourceType, always appending the memberEntitlement grant.

Comment thread pkg/connector/workspace_guard_test.go Outdated
Comment on lines +11 to +26
// Every grant the workspace syncer emits targets workspace_role, so when that
// type is excluded from the sync it must emit nothing — and must not page
// through users to discover that.
func TestWorkspaceBuilder_Grants_SkipWorkspaceRole(t *testing.T) {
// A nil client would panic if the guard failed to short-circuit.
b := workspaceBuilder(nil, nil, true)

res, err := resources.NewResource("acme", resourceTypeWorkspace, "T123")
if err != nil {
t.Fatalf("NewResource: %v", err)
}

grants, results, err := b.Grants(context.Background(), res, resources.SyncOpAttrs{})
if err != nil {
t.Fatalf("Grants: %v", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Suggestion: This test asserts the behavior flagged above — that zero grants are emitted — so it locks in the loss of workspace member grants. Once the guard is narrowed to only the workspace_role grants, this should assert that the member grants still come through (e.g. with a fake/stubbed user list) rather than that Grants returns nothing.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Connector PR Review: Guard workspace_role grant emission behind WillSyncResourceType

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

Review Summary

The new commit replaces the tautological TestWorkspaceRoleGrants_ExcludesWorkspaceMemberGrant with TestWorkspaceGrants_MemberGrantsSurviveRoleFilter, which stubs users.list via slack.OptionAPIURL and drives workspaceResourceType.Grants at both settings of skipWorkspaceRoleResourceType — that directly covers the regression the prior review said was untested, so that finding is addressed (the stub returns no response_metadata.next_cursor, so slack-go's auto-pagination terminates and the test cannot hang). The full PR diff was re-scanned for security and correctness: New computes the flag with a nil-safe check and WillSyncResourceType returns true when no explicit filter is set, member grants are emitted outside the gate, and no dependency manifests changed. One prior suggestion — the stale PR description — is still open.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • PR description (carried over, previously reported, still unaddressed) — it still says "Every grant the workspace syncer emits targets workspace_role, so Grants returns early when that type is filtered out — which also skips paging through workspace users", which is the design commit 4014f386 reverted; it also describes per-target guards plus a SkipEntitlementsAndGrants annotation added to the user syncer, which this diff does not contain (pkg/connector/user.go is untouched).
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In the pull request description (not a source file):
- Rewrite the description to match the code that is actually in this PR. Remove the
  claim that `workspaceResourceType.Grants` "returns early" / "skips paging through
  workspace users" when `workspace_role` is filtered out — commit 4014f386 changed this
  so only `workspaceRoleGrants` is gated and the workspace `member` grant is emitted
  unconditionally for every non-stranger user. Also remove the paragraph about guarding
  each target individually in the user syncer's Grants() and annotating the user
  resource type with SkipEntitlementsAndGrants when all targets are excluded:
  pkg/connector/user.go is not modified by this PR, and resourceTypeUser already carries
  SkipEntitlementsAndGrants unconditionally in pkg/connector/resource_types.go.

@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.

Blocking issues found — see review comments.

The previous commit short-circuited workspaceResourceType.Grants entirely
when workspace_role was excluded from the sync filter. That method emits a
mix, though: workspace_role assignments *and* the workspace's own member
grants. Returning early dropped every workspace membership.

Gate only the role grants. workspaceRoleGrants now holds the flag-to-role
mapping so it can be skipped wholesale, while the member grant is emitted
unconditionally.

The skip annotations (SkipGrants, SkipEntitlementsAndGrants) are not usable
here for the same reason: they suppress the resource's whole grants pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread pkg/connector/workspace_guard_test.go Outdated
Comment on lines +41 to +60
func TestWorkspaceRoleGrants_ExcludesWorkspaceMemberGrant(t *testing.T) {
workspaceID := &v2.ResourceId{ResourceType: resourceTypeWorkspace.Id, Resource: "T123"}
userID := &v2.ResourceId{ResourceType: resourceTypeUser.Id, Resource: "U123"}

workspace, err := resources.NewResource("acme", resourceTypeWorkspace, "T123")
if err != nil {
t.Fatalf("NewResource: %v", err)
}
memberGrant := grant.NewGrant(workspace, memberEntitlement, userID)

grants, err := workspaceRoleGrants(context.Background(), client.User{ID: "U123"}, workspaceID, userID)
if err != nil {
t.Fatalf("workspaceRoleGrants: %v", err)
}
for _, g := range grants {
if g.GetId() == memberGrant.GetId() {
t.Fatal("workspace member grant must be emitted unconditionally, not from the gated role helper")
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Suggestion: This test can never fail — grant IDs are entitlementID:principalType:principalID, and every grant workspaceRoleGrants returns is built on a workspace_role resource, so its ID can never equal the workspace:T123:member:... ID. It is a strictly weaker restatement of TestWorkspaceRoleGrants_OnlyTargetsWorkspaceRole above. The behavior actually worth pinning is the one the previous revision got wrong: that Grants still emits the workspace member grant for each non-stranger user when skipWorkspaceRoleResourceType is true. That needs a workspaceResourceType with a stubbed user source (fake businessPlusClient or an httptest server) rather than the helper in isolation — as written, Grants itself is untested at both settings of the flag.

@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.

TestWorkspaceRoleGrants_ExcludesWorkspaceMemberGrant could never fail: grant
IDs are entitlementID:principalType:principalID, and every grant the helper
returns is built on a workspace_role resource, so its ID could never equal the
workspace member grant's. It was a weaker restatement of
TestWorkspaceRoleGrants_OnlyTargetsWorkspaceRole, and Grants itself was
untested at both settings of skipWorkspaceRoleResourceType.

Replace it with a test that stubs users.list via slack.OptionAPIURL and drives
Grants both ways, asserting the workspace member grants survive when
workspace_role is filtered out. Verified it fails if the member grant is put
back inside the gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Aug 13, 2026

Copy link
Copy Markdown

CE-1169

@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.

@laurenleach
laurenleach merged commit d46e90e into main Aug 14, 2026
11 checks passed
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.

2 participants