Skip to content

Conversation

@aldevv
Copy link
Contributor

@aldevv aldevv commented Dec 6, 2025

Summary by CodeRabbit

  • Breaking Changes
    • Config field renamed "Enterprise Token" → "Business Plus Token"; Enterprise Grid-specific workspace/role APIs and enterprise role resources removed.
  • New Features
    • SCIM-based Business+ user and group sync, listing, enable/disable flows and updated workspace membership handling.
  • Chores
    • Dependency cleanup and client consolidation; unified error-wrapping and simplified client initialization.
  • Tests
    • Workspace grant/revoke tests removed.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Walkthrough

Replaces Enterprise Grid–centric APIs with a Business+ (SCIM) client: removes enterprise workspace/role resources and interfaces, rewires builders/connectors to use a businessPlus SCIM client, centralizes SCIM requests and error wrapping, and updates configuration to use BusinessPlusToken.

Changes

Cohort / File(s) Summary
Dependency changes
go.mod
Removed several test/enterprise-related indirect dependencies and added indirect github.com/stretchr/objx v0.5.2.
Configuration schema
pkg/config/conf.gen.go, pkg/config/config.go
Replaced enterprise-token/sso-enabled with business-plus-token; renamed EnterpriseTokenFieldBusinessPlusTokenField; updated display names, descriptions, and field relationships.
Client package & SCIM core
pkg/connector/client/...
pkg/connector/client/helpers.go, pkg/connector/client/mock.go, pkg/connector/client/models.go, pkg/connector/client/path.go, pkg/connector/client/request.go, pkg/connector/client/slack.go
Package renamed from enterpriseclient; removed many enterprise-specific types/constants/paths; added UrlPathIDPUsers; centralized SCIM requests via doScimRequest; simplified NewClient signature; introduced SCIM-focused methods and tightened error/response handling.
Removed enterprise abstraction
pkg/connector/client/slack_interface.go
Deleted SlackEnterpriseService interface, implementation, constructor, and related wrappers.
Connector wiring / client swap
pkg/connector/connector.go, pkg/connector/actions.go
Replaced enterprise client wiring with businessPlusClient; NewSlack/New now accept BusinessPlusToken; enable/disable user flows updated to use Business+ SCIM client; error wrapping adjusted.
Resource type removals
pkg/connector/enterprise_roles.go, pkg/connector/roles.go, pkg/connector/resource_types.go
Removed enterprise-role and workspace-role implementations, constants, resource registrations, and all related grant/entitlement logic.
Resources switched to SCIM / client wiring
pkg/connector/user.go, pkg/connector/group.go, pkg/connector/user_group.go, pkg/connector/workspace.go
Builders now accept businessPlusClient; user/group listing prefers SCIM when configured (new SCIM helpers); grant/revoke behaviours removed or adapted to SCIM; error wrapping standardized and pagination defaults switched to client package.
Helpers & pagination
pkg/helpers.go
Removed EnterpriseRolesPagination and related helpers; renamed WrapSlackClientErrorWrapError; updated token parsing and error-wrapping behavior.
Tests removed
pkg/connector/workspace_test.go
Deleted workspace grant/revoke tests and related mocks/helpers.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Config as Config
    participant Connector as Connector / ResourceSyncer
    participant Client as BusinessPlus SCIM Client
    participant SlackAPI as Slack SCIM API

    Config->>Connector: provide BusinessPlusToken
    Connector->>Client: NewClient(token)
    Connector->>Client: ListIDPUsers(startIndex, count)
    Client->>SlackAPI: GET /scim/{org}/Users (Authorization: token)
    SlackAPI-->>Client: 200 OK (SCIM users + pagination)
    Client-->>Connector: SCIMResponse[UserResource] + rateLimit
    Connector->>Connector: map SCIM users -> internal Resources
    Connector->>Config: SetWorkspaceNames(session store)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–60 minutes

  • Files needing extra attention:
    • pkg/connector/client/request.go — SCIM request centralization, error wrapping, response handling.
    • pkg/connector/client/slack.go — many public-signature changes and removed/renamed methods.
    • pkg/connector/user.go, pkg/connector/group.go, pkg/connector/workspace.go — SCIM vs standard API branching, pagination/token semantics, grant/revoke behavior changes.
    • Deleted files pkg/connector/enterprise_roles.go and pkg/connector/roles.go — verify no dangling registrations or references.

Possibly related PRs

Suggested reviewers

  • btipling
  • laurenleach
  • johnallers
  • agustin-conductor

Poem

🐰 I hopped from Grid to Business+ bright,
Tokens renamed and SCIM calls take flight,
Old roles trimmed, new endpoints sing,
I nibbled diffs beneath the moonlight,
And left a crunchy carrot-shaped log of delight. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'BB-1829 business plus support' clearly summarizes the main change in the changeset, which involves removing Enterprise Grid support and replacing it with Business+ client integration across the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch business_plus_and_actions_fix

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
pkg/connector/client/models.go (1)

89-89: Incorrect type for Roles field.

The Roles field is typed as []Email, which appears to be a copy-paste error. SCIM roles typically have value, display, and type fields, not email-specific fields like primary. Consider defining a dedicated Role type or reusing Group if the structure matches.

+type Role struct {
+	Value   string `json:"value"`
+	Display string `json:"display,omitempty"`
+	Type    string `json:"type,omitempty"`
+	Primary bool   `json:"primary,omitempty"`
+}
+
 type UserResource struct {
 	// ... other fields ...
-	Roles                                             []Email                                                `json:"roles"`
+	Roles                                             []Role                                                 `json:"roles"`
 	// ... other fields ...
 }
pkg/connector/client/request.go (1)

100-100: Redundant address-of operator on target.

Taking the address of target (already an interface{}) creates a double pointer if the caller passes a pointer type. This can cause unmarshaling issues. The same pattern appears in patchScimBytes (line 118) and patchScim (line 137).

 func (c *Client) getScim(
 	ctx context.Context,
 	path string,
 	target interface{},
 	queryParameters map[string]interface{},
 ) (
 	*v2.RateLimitDescription,
 	error,
 ) {
 	return c.doRequest(
 		ctx,
 		http.MethodGet,
 		c.getUrl(path, queryParameters, true),
-		&target,
+		target,
 		WithBearerToken(c.token),
 	)
 }

Apply the same fix to patchScimBytes (line 118) and patchScim (line 137).

pkg/connector/user.go (1)

29-92: Performance impact of additional per-user API calls in SCIM flow is real but architectural constraints limit optimization.

scimUserResource fetches full user info via GetUserInfoContext for each SCIM user because the SCIM API (ListIDPUsers) returns only limited user data (name, emails, etc.) and lacks critical fields like admin status, bot flags, MFA status, and user profile details.

The existing comment at line 30 correctly identifies this as a compatibility measure. However:

  • Batching is not viable: Slack API provides no batch users.info endpoint
  • Using SCIM data directly is not viable: SCIM UserResource lacks IsAdmin, IsOwner, IsBot, Has2FA, Profile fields, and other required attributes
  • Consider documenting: If this is planned to be addressed, clarify the timeline or constraints preventing optimization

If performance becomes critical, the only architectural alternative would be switching from the SCIM provisioning API to the standard users.list API for the entire user sync flow.

🧹 Nitpick comments (10)
pkg/connector/actions.go (1)

122-164: Consider consolidating the duplicate handler logic.

The handleDisableUser and handleEnableUser functions share nearly identical structure—they differ only in the operation name and the client method called. This duplication could be reduced by extracting a common handler function.

Example refactor to reduce duplication:

// handleUserStatusChange is a generic handler for enable/disable operations
func (s *Slack) handleUserStatusChange(
	ctx context.Context,
	args *structpb.Struct,
	operation string,
	clientFunc func(context.Context, string) (*v2.RateLimitDescription, error),
) (*structpb.Struct, annotations.Annotations, error) {
	l := ctxzap.Extract(ctx)

	userIDValue, ok := args.Fields["user_id"]
	if !ok {
		return nil, nil, fmt.Errorf("user_id parameter is required")
	}

	userID := userIDValue.GetStringValue()
	if userID == "" {
		return nil, nil, fmt.Errorf("user_id cannot be empty")
	}

	l.Debug(fmt.Sprintf("%s user via SCIM", operation), zap.String("user_id", userID))

	if s.businessPlusClient == nil {
		return nil, nil, fmt.Errorf("business+ client not available - SCIM API requires Business+ plan")
	}

	ratelimitData, err := clientFunc(ctx, userID)
	if err != nil {
		l.Error(fmt.Sprintf("failed to %s user", operation), zap.String("user_id", userID), zap.Error(err))
		return nil, nil, fmt.Errorf("failed to %s user %s: %w", operation, userID, err)
	}

	outputAnnotations := annotations.New()
	if ratelimitData != nil {
		outputAnnotations.WithRateLimiting(ratelimitData)
	}

	l.Info(fmt.Sprintf("user %s successfully", operation), zap.String("user_id", userID))

	return &structpb.Struct{
		Fields: map[string]*structpb.Value{
			"success": {Kind: &structpb.Value_BoolValue{BoolValue: true}},
			"message": {Kind: &structpb.Value_StringValue{StringValue: fmt.Sprintf("User %s %s successfully", userID, operation)}},
			"user_id": {Kind: &structpb.Value_StringValue{StringValue: userID}},
		},
	}, outputAnnotations, nil
}

// handleDisableUser deactivates a Slack user by setting active to false via SCIM API.
func (s *Slack) handleDisableUser(ctx context.Context, args *structpb.Struct) (*structpb.Struct, annotations.Annotations, error) {
	return s.handleUserStatusChange(ctx, args, "disable", s.businessPlusClient.DisableUser)
}

// handleEnableUser activates a Slack user by setting active to true via SCIM API.
func (s *Slack) handleEnableUser(ctx context.Context, args *structpb.Struct) (*structpb.Struct, annotations.Annotations, error) {
	return s.handleUserStatusChange(ctx, args, "enable", s.businessPlusClient.EnableUser)
}

Also applies to: 166-209

pkg/connector/client/helpers.go (1)

11-25: Consider logging level and potential sensitive data exposure.

The logBody helper logs response body content at Info level. Response bodies may contain sensitive information (tokens, user data, error details with PII). Consider using Debug level instead for production safety.

-	l.Info("response body: ", zap.String("body", string(body)))
+	l.Debug("response body: ", zap.String("body", string(body)))
pkg/connector/client/request.go (2)

252-254: Simplify error creation.

fmt.Errorf("%s", detail) can be simplified to errors.New(detail) since there's no formatting involved.

+import "errors"
+
 		if detail, ok := errorResponse["detail"].(string); ok {
-			return &ratelimitData, uhttp.WrapErrors(codes.Internal, "SCIM API error", fmt.Errorf("%s", detail))
+			return &ratelimitData, uhttp.WrapErrors(codes.Internal, "SCIM API error", errors.New(detail))
 		}

211-217: Minor: Duplicate getUrl call.

getUrl is called twice in deleteScim - once for logging (line 211) and once for the request (line 217). Consider storing the result in a variable to avoid redundant computation.

 func (c *Client) deleteScim(
 	ctx context.Context,
 	path string,
 ) (
 	*v2.RateLimitDescription,
 	error,
 ) {
+	deleteUrl := c.getUrl(path, nil, true)
 	logger := ctxzap.Extract(ctx)
 	logger.Debug(
 		"making request",
 		zap.String("method", http.MethodDelete),
-		zap.String("url", c.getUrl(path, nil, true).String()),
+		zap.String("url", deleteUrl.String()),
 	)

 	request, err := c.wrapper.NewRequest(
 		ctx,
 		http.MethodDelete,
-		c.getUrl(path, nil, true),
+		deleteUrl,
 		WithBearerToken(c.token),
 		uhttp.WithAcceptJSONHeader(),
 	)
pkg/config/config.go (1)

23-28: Deprecated field still defined but unused.

SSOEnabledField is marked as deprecated but still defined in the package. Since it's not included in ConfigurationFields (line 39-43), it won't be exposed to users. Consider removing this field definition entirely to avoid confusion, or document why it's retained (e.g., backward compatibility for existing configs).

pkg/connector/connector.go (1)

123-135: Variable shadowing and redundant error prefix.

Two minor issues:

  1. Line 125-126: The err variable is pre-declared on line 125, but line 126 uses := which shadows it. Use = instead for clarity.
  2. Line 133: The error message includes "Error:" which is redundant when using %w formatting.
 	var businessPlusClient *client.Client
 	if businessPlusKey != "" {
-		var err error
-		businessPlusClient, err = client.NewClient(
+		var clientErr error
+		businessPlusClient, clientErr = client.NewClient(
 			httpClient,
 			businessPlusKey,
 			apiKey,
 			govEnv,
 		)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create Business+ client. Error: %w", err)
+		if clientErr != nil {
+			return nil, fmt.Errorf("failed to create Business+ client: %w", clientErr)
 		}
 	}
pkg/connector/workspace.go (1)

159-161: Potential double-wrapping of errors may overwrite gRPC codes.

Based on the retrieved learnings, when client methods like GetUsers already wrap errors with specific gRPC codes via handleError(), using uhttp.WrapErrors again could overwrite those codes (e.g., Unavailable, PermissionDenied) needed for proper retry logic and alerting.

Consider using fmt.Errorf with %w to preserve the underlying error codes:

 	if err != nil {
-		return nil, nil, uhttp.WrapErrors(codes.Internal, "fetching users for workspace", err)
+		return nil, nil, fmt.Errorf("fetching users for workspace: %w", err)
 	}
pkg/connector/user.go (2)

37-55: Code duplication in profile building.

The profile map construction is nearly identical between scimUserResource (lines 37-55) and userResource (lines 100-118). Consider extracting a helper function to reduce duplication:

func buildUserProfile(user *slack.User) map[string]interface{} {
    profile := make(map[string]interface{})
    profile["first_name"] = user.Profile.FirstName
    // ... rest of profile fields
    return profile
}

This would also ensure both paths stay in sync if profile fields change.

Also applies to: 100-118


242-242: Consider using the PageSizeDefault constant.

The count value is hardcoded to 100. Consider using client.PageSizeDefault for consistency:

-	count := 100
+	count := client.PageSizeDefault
pkg/connector/client/slack.go (1)

413-461: Consider using SCIM "remove" operation for efficiency.

The current implementation fetches all members, filters locally, then replaces the entire member list. For large groups, this could be inefficient.

SCIM 2.0 supports a direct "remove" operation:

Operations: []ScimOperate{
    {
        Op:   "remove",
        Path: fmt.Sprintf("members[value eq \"%s\"]", user),
    },
}

However, the current approach may be more compatible across SCIM implementations. If this is intentional, consider adding a comment explaining the design choice.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 233753e and fc9aac2.

⛔ Files ignored due to path filters (37)
  • .github/workflows/ci.yaml is excluded by none and included by none
  • README.md is excluded by none and included by none
  • go.sum is excluded by !**/*.sum and included by none
  • vendor/github.com/davecgh/go-spew/LICENSE is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/bypass.go is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/bypasssafe.go is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/common.go is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/config.go is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/doc.go is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/dump.go is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/format.go is excluded by !vendor/** and included by none
  • vendor/github.com/davecgh/go-spew/spew/spew.go is excluded by !vendor/** and included by none
  • vendor/github.com/pmezard/go-difflib/LICENSE is excluded by !vendor/** and included by none
  • vendor/github.com/pmezard/go-difflib/difflib/difflib.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/LICENSE is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/assertion_compare.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/assertion_format.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/assertion_forward.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/assertion_order.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/assertions.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/doc.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/errors.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/forward_assertions.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/http_assertions.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/yaml/yaml_custom.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/yaml/yaml_default.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/assert/yaml/yaml_fail.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/require/doc.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/require/forward_requirements.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/require/require.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/require/require.go.tmpl is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/require/require_forward.go is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/require/require_forward.go.tmpl is excluded by !vendor/** and included by none
  • vendor/github.com/stretchr/testify/require/requirements.go is excluded by !vendor/** and included by none
  • vendor/modules.txt is excluded by !vendor/** and included by none
📒 Files selected for processing (21)
  • go.mod (1 hunks)
  • pkg/config/conf.gen.go (1 hunks)
  • pkg/config/config.go (2 hunks)
  • pkg/connector/actions.go (2 hunks)
  • pkg/connector/client/helpers.go (1 hunks)
  • pkg/connector/client/mock.go (1 hunks)
  • pkg/connector/client/models.go (1 hunks)
  • pkg/connector/client/path.go (2 hunks)
  • pkg/connector/client/request.go (6 hunks)
  • pkg/connector/client/slack.go (5 hunks)
  • pkg/connector/client/slack_interface.go (0 hunks)
  • pkg/connector/connector.go (5 hunks)
  • pkg/connector/enterprise_roles.go (0 hunks)
  • pkg/connector/group.go (10 hunks)
  • pkg/connector/resource_types.go (0 hunks)
  • pkg/connector/roles.go (0 hunks)
  • pkg/connector/user.go (4 hunks)
  • pkg/connector/user_group.go (3 hunks)
  • pkg/connector/workspace.go (5 hunks)
  • pkg/connector/workspace_test.go (0 hunks)
  • pkg/helpers.go (4 hunks)
💤 Files with no reviewable changes (5)
  • pkg/connector/client/slack_interface.go
  • pkg/connector/workspace_test.go
  • pkg/connector/resource_types.go
  • pkg/connector/roles.go
  • pkg/connector/enterprise_roles.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-28T17:48:53.644Z
Learnt from: mateoHernandez123
Repo: ConductorOne/baton-slack PR: 57
File: pkg/connector/roles.go:217-219
Timestamp: 2025-10-28T17:48:53.644Z
Learning: In the baton-slack connector, when client methods like SetWorkspaceRole(), RemoveUser(), or other enterprise/slack client calls already wrap errors with specific gRPC codes via handleError(), use fmt.Errorf with %w to preserve those codes while adding context. Only use uhttp.WrapErrors for errors generated directly in the function (e.g., validation, precondition checks). Double-wrapping with uhttp.WrapErrors would overwrite the specific codes (Unavailable, PermissionDenied, etc.) needed for proper retry logic and alerting.
<!-- [/add_learning]

Applied to files:

  • pkg/connector/actions.go
  • pkg/connector/connector.go
  • pkg/connector/client/request.go
  • pkg/connector/client/models.go
  • pkg/connector/workspace.go
  • pkg/connector/client/path.go
  • pkg/helpers.go
  • pkg/connector/user_group.go
  • pkg/connector/group.go
  • pkg/connector/client/slack.go
🧬 Code graph analysis (4)
pkg/connector/user_group.go (3)
pkg/connector/client/slack.go (1)
  • Client (32-39)
pkg/connector/connector.go (1)
  • New (145-158)
pkg/helpers.go (1)
  • WrapError (173-222)
pkg/connector/group.go (3)
pkg/connector/client/slack.go (2)
  • Client (32-39)
  • PageSizeDefault (21-21)
pkg/connector/client/models.go (1)
  • GroupResource (148-154)
pkg/connector/connector.go (1)
  • New (145-158)
pkg/connector/client/slack.go (2)
pkg/connector/client/models.go (3)
  • SCIMResponse (18-24)
  • GroupResource (148-154)
  • UserResource (72-94)
pkg/connector/client/path.go (2)
  • UrlPathIDPGroups (22-22)
  • UrlPathIDPUsers (24-24)
pkg/connector/user.go (3)
pkg/connector/client/slack.go (1)
  • Client (32-39)
pkg/connector/client/models.go (4)
  • UserResource (72-94)
  • Email (103-106)
  • Name (118-122)
  • User (33-59)
pkg/helpers.go (2)
  • WrapError (173-222)
  • AnnotationsForError (227-242)
🔇 Additional comments (37)
pkg/connector/actions.go (2)

140-148: Client migration to businessPlusClient is correct.

The switch from enterpriseClient to businessPlusClient is straightforward and verified. The businessPlusClient field exists on the Slack struct as *client.Client, and both DisableUser() and EnableUser() methods exist with the correct signatures accepting (ctx context.Context, userID string) and returning (*v2.RateLimitDescription, error).

Error handling correctly uses fmt.Errorf with %w to preserve gRPC error codes from the client's internal error wrapping, ensuring proper retry logic and alerting. This approach aligns with best practices for error propagation.

Both handlers follow identical patterns and could benefit from optional refactoring to reduce duplication, but the current implementation is clear and functional.


185-193: Client migration looks correct.

The changes mirror those in handleDisableUser and maintain consistency across parameter validation, error handling, and response formatting. The error handling correctly preserves wrapped error codes using the %w format specifier, and the EnableUser method signature on the business plus client is confirmed.

pkg/connector/client/mock.go (1)

9-21: Remove unused mock implementation or implement the corresponding interface.

The MockSlackEnterpriseService is dead code—it's never imported or used anywhere in the codebase, and it references methods (AddUser, RemoveUser) and an interface (SlackEnterpriseService) that don't exist. Either remove this mock or implement the interface it's intended to mock.

Likely an incorrect or invalid review comment.

pkg/connector/client/helpers.go (1)

1-1: Package rename aligns with the Business+ refactor.

The package declaration change from enterprise to client is consistent with the broader migration.

pkg/connector/client/models.go (2)

1-1: Package rename consistent with the refactor.

The package declaration aligns with the migration to the Business+ client architecture.


156-165: The ScimOperate struct is used exclusively for group membership operations in the codebase, where the Value field always contains a []UserID array. The current typing is appropriate and does not impose unnecessary constraints.

go.mod (2)

87-87: Indirect dependency addition looks reasonable.

The stretchr/objx package is typically pulled in as an indirect dependency of testify/mock. This addition appears consistent with the dependency tree.


3-3: Go 1.25.2 is a valid release version. As of December 2025, Go 1.25.5 is the latest stable release, making 1.25.2 a standard patch release in the current 1.25 series.

pkg/connector/client/request.go (2)

1-1: Package rename consistent with refactor.

The package declaration aligns with the Business+ client migration.


171-173: Error wrapping approach is appropriate.

Using uhttp.WrapErrors for errors generated directly in request handling (HTTP creation, body reading, unmarshaling) is correct per the established pattern. This preserves proper error categorization for retry logic. Based on learnings.

pkg/connector/client/path.go (1)

1-25: Path constants properly streamlined for Business+ architecture.

The removal of enterprise-specific paths (role assignments, admin operations) and retention of standard Slack API and SCIM paths aligns well with the Business+ refactor. The SCIM paths correctly use format strings for team ID substitution.

pkg/helpers.go (2)

171-222: LGTM! Function rename and error handling pattern are appropriate.

The rename from WrapSlackClientError to WrapError simplifies the API. The function correctly handles rate limit errors with codes.ResourceExhausted and maps other Slack errors using the centralized MapSlackErrorToGRPCCode function.


19-76: Error wrapping follows the established pattern.

Using uhttp.WrapErrors directly for validation errors (InvalidArgument in ParseID, ParseRole, ParsePageToken) and internal errors (Internal in MakeResourceList) is correct. Based on learnings, this approach is appropriate for errors generated directly in these functions rather than from Slack client calls.

pkg/config/config.go (1)

17-22: Configuration field and relationships are properly defined.

The BusinessPlusTokenField is correctly marked as a secret, and the description accurately reflects its purpose for both Business+ and Enterprise Grid contexts. The FieldRelationships correctly enforces that GovEnvironmentField depends on BusinessPlusTokenField.

Also applies to: 47-53

pkg/config/conf.gen.go (1)

6-10: Generated struct aligns with configuration changes.

The Slack struct fields (Token, BusinessPlusToken, GovEnv) correctly reflect the updated ConfigurationFields in config.go. Since this is generated code, no manual changes should be made here.

pkg/connector/user_group.go (2)

82-97: Error handling follows the correct pattern.

Using pkg.WrapError for the Slack client call (line 84) preserves the appropriate gRPC codes, while uhttp.WrapErrors with codes.Internal (line 92) is correct for resource conversion errors generated within this function. Based on learnings, this pattern avoids double-wrapping issues.


133-165: Grants method correctly handles errors from Slack API calls.

The error wrapping is consistent: pkg.WrapError for Slack client calls (GetUserGroupMembersContext, GetUserInfoContext) and uhttp.WrapErrors for internal resource creation. This preserves the proper gRPC codes for retry logic.

pkg/connector/connector.go (3)

72-93: Validate method follows correct error handling patterns.

Slack client errors (AuthTestContext, GetUserInfoContext) are wrapped with pkg.WrapError to preserve gRPC codes, while the permission validation error uses uhttp.WrapErrors directly. This aligns with the established pattern.


160-167: ResourceSyncers correctly wires the Business+ client to all builders.

The builders receive the appropriate clients based on their needs. The groupBuilder only needs businessPlusClient (likely for SCIM operations), while other builders need both the standard Slack client and the Business+ client.


145-158: Config mapping correctly uses the new BusinessPlusToken field.

The New function properly passes config.BusinessPlusToken to NewSlack, aligning with the configuration field rename from EnterpriseToken to BusinessPlusToken.

pkg/connector/workspace.go (3)

21-40: LGTM - Clean struct and builder refactor.

The struct and builder have been correctly updated to use the Business+ client pattern, replacing the enterprise client references.


68-110: LGTM - List method with appropriate error handling.

The List method correctly uses pkg.WrapError for Slack API errors (preserving gRPC codes) and uhttp.WrapErrors for local errors like token parsing and resource creation. This aligns with the established error handling pattern.


187-189: Good documentation on API limitations.

The comment clearly explains the Enterprise Grid API requirement for workspace membership management, which is helpful for future maintainers.

pkg/connector/group.go (5)

26-42: LGTM - Clean struct and builder refactor for groups.

The struct fields and builder signature have been correctly simplified to use the Business+ client pattern.


96-136: LGTM - List method with proper nil check and error handling.

The graceful degradation when businessPlusClient is nil and the error wrapping pattern are correct. Since ListIDPGroups uses fmt.Errorf (not handleError), applying uhttp.WrapErrors here is appropriate.


171-204: LGTM - Grants method correctly handles IDP group members.

The error wrapping is appropriate since GetIDPGroup uses fmt.Errorf without gRPC codes.


206-250: LGTM - Grant method with proper precondition checks.

The precondition checks for nil client, gov environment, and principal type are well-structured. Error wrapping is appropriate.


252-303: LGTM - Revoke method mirrors Grant structure.

Consistent precondition checks and the GrantAlreadyRevoked annotation handling is a good touch for user experience.

pkg/connector/user.go (3)

181-202: LGTM - Clean routing between SCIM and standard API.

The List method provides clear routing with helpful debug logging for troubleshooting.


204-229: LGTM - Standard API listing with proper error handling.

The use of pkg.AnnotationsForError correctly handles rate limiting from the Slack API.


258-261: LGTM - Correct SCIM pagination logic.

The pagination calculation correctly handles SCIM's 1-based indexing.

pkg/connector/client/slack.go (6)

32-39: LGTM - Clean Client struct definition.

The struct fields are well-organized for supporting both standard Slack API and SCIM endpoints.


41-74: LGTM - NewClient with gov environment support.

The constructor properly handles both standard and gov environment URL configurations. Token validation may be happening at the config level.


76-123: LGTM - Centralized error handling with gRPC code mapping.

Good use of the centralized MapSlackErrorToGRPCCode function. The context messages provide helpful debugging information.


125-132: LGTM - Clean standalone function for session storage.

Converting to a standalone function is appropriate since it doesn't require Client state.


304-330: LGTM - SCIM group listing with proper pagination.

The method correctly uses the SCIM version for URL path formatting and passes pagination parameters.


496-547: LGTM - Enable/Disable user via SCIM.

The asymmetric approach (DELETE for disable, PATCH for enable) correctly matches Slack's SCIM API behavior for user lifecycle management.

Comment on lines 20 to 39
type userGroupResourceType struct {
resourceType *v2.ResourceType
client *slack.Client
enterpriseID string
enterpriseClient *enterprise.Client
resourceType *v2.ResourceType
client *slack.Client
businessPlusClient *client.Client
}

func (o *userGroupResourceType) ResourceType(_ context.Context) *v2.ResourceType {
return o.resourceType
}

func userGroupBuilder(
client *slack.Client,
enterpriseID string,
enterpriseClient *enterprise.Client,
slackClient *slack.Client,
businessPlusClient *client.Client,
) *userGroupResourceType {
return &userGroupResourceType{
resourceType: resourceTypeUserGroup,
client: client,
enterpriseID: enterpriseID,
enterpriseClient: enterpriseClient,
resourceType: resourceTypeUserGroup,
client: slackClient,
businessPlusClient: businessPlusClient,
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'businessPlusClient' pkg/connector/user_group.go

Repository: ConductorOne/baton-slack

Length of output: 189


🏁 Script executed:

# Check if businessPlusClient is used in any methods or beyond assignment
rg -A 50 'func.*userGroupResourceType' pkg/connector/user_group.go | head -100

Repository: ConductorOne/baton-slack

Length of output: 2626


businessPlusClient field is declared but never used in this file.

The businessPlusClient field is assigned in the struct initialization but not referenced in any method (List, Entitlements, Grants). Remove it unless it's intended for future functionality in a way not yet implemented.

🤖 Prompt for AI Agents
In pkg/connector/user_group.go around lines 20 to 39, the businessPlusClient
field is declared on userGroupResourceType and set in userGroupBuilder but never
used; remove the unused field from the struct, remove the businessPlusClient
parameter from userGroupBuilder, and stop assigning it in the returned struct.
Also update any call sites of userGroupBuilder to no longer pass a
businessPlusClient argument so the code compiles cleanly.

@aldevv aldevv force-pushed the business_plus_and_actions_fix branch 2 times, most recently from 5e5fc9a to 204a1ef Compare December 6, 2025 02:49
…ne/baton-slack into business_plus_and_actions_fix
@aldevv aldevv force-pushed the business_plus_and_actions_fix branch from 204a1ef to 5d8f7d3 Compare December 6, 2025 03:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/connector/client/request.go (1)

105-122: Resolve unused patchScimBytes to fix golangci-lint failure

golangci-lint reports func (*Client).patchScimBytes is unused (unused) at Line 105. This will keep CI red until addressed.

If you no longer need the raw-[]byte SCIM patch variant, it’s simplest to remove it:

-func (c *Client) patchScimBytes(
-	ctx context.Context,
-	path string,
-	target interface{},
-	payload []byte,
-) (
-	*v2.RateLimitDescription,
-	error,
-) {
-	return c.doRequest(
-		ctx,
-		http.MethodPatch,
-		c.getUrl(path, nil, true),
-		&target,
-		WithBearerToken(c.token),
-		uhttp.WithJSONBody(payload),
-	)
-}
-

If you still need this behavior, consider wiring it up through the new Business+ SCIM paths so that it’s referenced somewhere, or add a comment and //nolint:unused only if it’s intentionally kept for future use (but removal is cleaner).

🧹 Nitpick comments (6)
pkg/connector/client/slack.go (4)

124-131: SetWorkspaceNames helper is fine; optional micro‑optimization

The helper correctly maps workspace IDs to names and stores them in the session. If this ever runs over many workspaces, you could pre‑size the map:

-workspaceMap := make(map[string]string)
+workspaceMap := make(map[string]string, len(workspaces))

Purely optional.


303-329: ListIDPGroups SCIM pagination and error handling are appropriate

The SCIM list call uses the expected startIndex/count query params and defers HTTP error classification to getScim, only adding context via fmt.Errorf, which avoids double‑wrapping gRPC codes. Based on learnings, this is the right pattern relative to uhttp.WrapErrors.

You might consider normalizing obviously invalid inputs (e.g., coercing startIndex <= 0 to 1 or defaulting count when <= 0) to avoid avoidable 4xx responses from Slack.


331-357: ListIDPUsers mirrors ListIDPGroups; potential DRY helper

Implementation mirrors ListIDPGroups and looks correct from a SCIM and error‑handling standpoint. If you find yourself adding more SCIM list endpoints, consider a small generic helper like listScim[T any](path string, startIndex, count int) to remove the duplication of the SCIMResponse[T] boilerplate.


391-405: AddUserToGroup is correct and idempotent; you can simplify the PATCH payload

The flow (fetch group → short‑circuit if user already a member → PATCH) is correct and idempotent. For the PATCH body, you don't need to resend all existing members; SCIM semantics allow you to send only the new member. That would reduce payload size and the chance of subtle races with concurrent membership changes:

-	var members []UserID
-	for _, member := range group.Members {
-		if member.Value == user {
-			return ratelimitData, nil
-		}
-		members = append(members, UserID{Value: member.Value})
-	}
-
-	members = append(members, UserID{Value: user})
+	for _, member := range group.Members {
+		if member.Value == user {
+			// already present, nothing to do
+			return ratelimitData, nil
+		}
+	}
+
+	members := []UserID{{Value: user}}

The surrounding error wrapping with fmt.Errorf("...: %w", err) is consistent with the existing HTTP wrappers and preserves underlying gRPC codes. Based on learnings, ...

Also applies to: 410-413, 417-417

pkg/connector/client/request.go (2)

200-225: doScimRequest helper looks good; consider migrating SCIM helpers to it

The new doScimRequest nicely centralizes:

  • SCIM base URL selection (c.getUrl(path, nil, true)),
  • bearer token handling (WithBearerToken(c.token)),
  • and optional JSON payload wiring.

This will help keep Business+ SCIM flows consistent.

Follow-up suggestion (non-blocking):

  • Over time, consider refactoring getScim/patchScim (and any new SCIM verbs) to delegate to doScimRequest for consistency and to avoid repeating token and URL logic.
  • If you end up needing query parameters for some SCIM GETs, you might extend doScimRequest with an optional queryParameters argument instead of adding more one-off wrappers.

227-285: SCIM delete error wrapping is consistent but could map codes more precisely (optional)

The deleteScim flow now:

  • Wraps request creation and body/error parsing failures with uhttp.WrapErrors(codes.Internal, ...).
  • Logs and returns the underlying error from wrapper.Do directly for transport/HTTP failures (preserving any mapping wrapper.Do already applies).
  • Extracts SCIM "detail" and wraps it as a codes.Internal error.

This is consistent with the earlier learning about avoiding double-wrapping uhttp.WrapErrors around already-coded errors (you only wrap errors created here, and leave wrapper.Do’s own codes intact).

Optional improvement:

  • If you care about distinguishing common SCIM failures at the gRPC layer (e.g., 404 → codes.NotFound, 403 → codes.PermissionDenied, 429/503 → codes.Unavailable), you could branch on response.StatusCode and choose more specific codes.* when wrapping the "detail" message, rather than always codes.Internal.

Not a blocker, but might improve retry behavior and observability for downstream callers.

If you decide to refine the gRPC codes here, please double-check expectations from any existing callers (e.g., retries or alerts keyed on codes.Internal vs. more specific codes).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc9aac2 and 8382115.

⛔ Files ignored due to path filters (1)
  • .github/workflows/ci.yaml is excluded by none and included by none
📒 Files selected for processing (2)
  • pkg/connector/client/request.go (6 hunks)
  • pkg/connector/client/slack.go (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-28T17:48:53.644Z
Learnt from: mateoHernandez123
Repo: ConductorOne/baton-slack PR: 57
File: pkg/connector/roles.go:217-219
Timestamp: 2025-10-28T17:48:53.644Z
Learning: In the baton-slack connector, when client methods like SetWorkspaceRole(), RemoveUser(), or other enterprise/slack client calls already wrap errors with specific gRPC codes via handleError(), use fmt.Errorf with %w to preserve those codes while adding context. Only use uhttp.WrapErrors for errors generated directly in the function (e.g., validation, precondition checks). Double-wrapping with uhttp.WrapErrors would overwrite the specific codes (Unavailable, PermissionDenied, etc.) needed for proper retry logic and alerting.
<!-- [/add_learning]

Applied to files:

  • pkg/connector/client/request.go
  • pkg/connector/client/slack.go
🧬 Code graph analysis (2)
pkg/connector/client/request.go (1)
pkg/connector/client/slack.go (1)
  • Client (31-38)
pkg/connector/client/slack.go (2)
pkg/connector/client/models.go (6)
  • SCIMResponse (18-24)
  • GroupResource (148-154)
  • UserResource (72-94)
  • UserID (167-169)
  • PatchOp (156-159)
  • ScimOperate (161-165)
pkg/connector/client/path.go (3)
  • UrlPathIDPGroups (22-22)
  • UrlPathIDPUsers (24-24)
  • UrlPathIDPGroup (21-21)
🪛 GitHub Actions: ci
pkg/connector/client/request.go

[error] 105-105: golangci-lint run --timeout=3m: pkg/connector/client/request.go:105:18: func (*Client).patchScimBytes is unused (unused)

🔇 Additional comments (4)
pkg/connector/client/slack.go (3)

31-37: Client SCIM configuration and gov routing look solid

The Client struct and NewClient wiring (base URL vs SCIM URL, token/botToken, govEnv switching between v1/v2) are coherent and side‑effect free. No correctness issues stand out here.

Also applies to: 40-72


487-492: patchGroup’s switch to doScimRequest is consistent

Using doScimRequest with http.MethodPatch here is consistent with the other SCIM helpers, and the additional context from fmt.Errorf("error patching IDP group: %w", err) is appropriate while still delegating HTTP/grpc code mapping to the underlying client.


441-452: Update SCIM PATCH operation format for removing users from Slack groups

Slack's official SCIM documentation specifies that removing a user from a group requires a PATCH with "operation": "delete" inside the members array with the user's value included. The current approach using a separate remove operation with a filtered path does not match Slack's documented format and may fail or be ignored.

Verify that the PATCH request includes members array with objects containing "value": "user-id" and "operation": "delete", as shown in Slack's SCIM Groups PATCH documentation.

pkg/connector/client/request.go (1)

87-141: Fix JSON unmarshalling target usage in doRequest and its SCIM callers

The review identifies a theoretical issue with how doRequest handles the target parameter when unmarshalling JSON. According to Go's json.Unmarshal semantics, passing a pointer to an interface{} variable prevents the unmarshaller from modifying the original caller's destination struct or map—instead, the unmarshaller replaces the interface value with a newly allocated map or other representation.

The review claims:

  • doRequest calls json.Unmarshal(bodyBytes, &target) where target is interface{}
  • Callers (getScim, patchScimBytes, patchScim) pass &target to doRequest
  • This causes the caller's destination to remain unpopulated

The suggested fix is to have callers pass target (not &target) and doRequest unmarshal into target (not &target), allowing the unmarshaller to populate the caller's pointer correctly.

The fix logic is sound according to Go semantics, but actual verification of the bug's presence in the current code is required, as the implementation of doRequest itself and the real caller patterns outside this file cannot be inspected.

Additionally, patchScimBytes is reported as unused; confirm whether it should be removed to resolve the golangci-lint failure.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
pkg/connector/user.go (1)

42-55: Remove redundant profile["user_id"] assignments in both flows

profile["user_id"] is assigned twice in both scimUserResource and userResource; the second fmt.Sprint(...) write is redundant because slackUser.ID / user.ID is already a string. Keeping both only adds noise.

Suggested change:

@@ func (o *userResourceType) scimUserResource(...)
-    profile["is_stranger"] = slackUser.IsStranger
-    profile["is_deleted"] = slackUser.Deleted
-    profile["user_id"] = fmt.Sprint(slackUser.ID)
+    profile["is_stranger"] = slackUser.IsStranger
+    profile["is_deleted"] = slackUser.Deleted
@@ func userResource(...)
-    profile["is_stranger"] = user.IsStranger
-    profile["is_deleted"] = user.Deleted
-    profile["user_id"] = fmt.Sprint(user.ID)
+    profile["is_stranger"] = user.IsStranger
+    profile["is_deleted"] = user.Deleted

Also applies to: 105-118

🧹 Nitpick comments (3)
pkg/connector/user.go (3)

29-35: SCIM list path does an N+1 Slack Web API call per user

listScimAPI pages through SCIM users, and for each one scimUserResource does GetUserInfoContext to fetch full Slack user data. For large workspaces this becomes an N+1 pattern (1 SCIM call + N Web API calls per page) and will hit Slack rate limits quickly.

If client.UserResource already includes the needed fields (names, email, status, etc.), consider:

  • Building the profile and UserTrait primarily from the SCIM object.
  • Falling back to GetUserInfoContext only when specific non‑SCIM fields are required or missing.
  • Optionally making the extra Slack lookup feature‑flagged or configurable if strict parity with the non‑SCIM profile isn’t always needed.

This keeps Business+ SCIM listing performant while preserving compatibility where necessary.

Also applies to: 243-247, 249-255


245-247: Verify whether ListIDPUsers errors are already wrapped before calling uhttp.WrapErrors

Here errors from o.businessPlusClient.ListIDPUsers are wrapped again with uhttp.WrapErrors(codes.Internal, ...). If ListIDPUsers (or its underlying client) already maps Slack errors to specific gRPC codes via handleError / uhttp.WrapErrors, this will overwrite those codes with Internal, which can break retry logic and alerting.

If ListIDPUsers already does that mapping, prefer preserving the original code and just adding context, e.g.:

return nil, &resource.SyncOpResults{Annotations: annos}, fmt.Errorf("fetching SCIM users: %w", err)

If it currently returns bare errors, your existing uhttp.WrapErrors here is appropriate.

Based on learnings, please double‑check the client implementation for this method.


231-263: Optional: consider honoring attrs.Limit in SCIM pagination

listScimAPI currently uses a fixed count := 100 for each SCIM page and only reads attrs.PageToken.Token. If Baton sends a smaller attrs.Limit, you could optionally cap the SCIM count to respect that limit:

count := 100
if attrs.Limit > 0 && int(attrs.Limit) < count {
    count = int(attrs.Limit)
}

Not required for correctness, but it can make paging behavior more predictable across connectors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 531a523 and ddfdb18.

📒 Files selected for processing (1)
  • pkg/connector/user.go (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-28T17:48:53.644Z
Learnt from: mateoHernandez123
Repo: ConductorOne/baton-slack PR: 57
File: pkg/connector/roles.go:217-219
Timestamp: 2025-10-28T17:48:53.644Z
Learning: In the baton-slack connector, when client methods like SetWorkspaceRole(), RemoveUser(), or other enterprise/slack client calls already wrap errors with specific gRPC codes via handleError(), use fmt.Errorf with %w to preserve those codes while adding context. Only use uhttp.WrapErrors for errors generated directly in the function (e.g., validation, precondition checks). Double-wrapping with uhttp.WrapErrors would overwrite the specific codes (Unavailable, PermissionDenied, etc.) needed for proper retry logic and alerting.
<!-- [/add_learning]

Applied to files:

  • pkg/connector/user.go
🔇 Additional comments (3)
pkg/connector/user.go (3)

194-202: List selection between SCIM and standard API looks good

The List method’s behavior of preferring SCIM when businessPlusClient is non‑nil and clearly logging which path is used is clean and predictable, with a safe early‑exit when parentResourceID is nil.


204-229: Standard (non‑SCIM) listing flow is consistent with existing patterns

Using GetUsersContext with GetUsersOptionTeamID, mapping through userResource, and threading AnnotationsForError up via SyncOpResults aligns with the existing connector style. The internal userResource errors are correctly wrapped with uhttp.WrapErrors(codes.Internal, ...) since they originate locally in this code.


265-273: Builder wiring for Slack and Business+ clients looks straightforward

userBuilder cleanly initializes userResourceType with both the Slack client and the optional Business+ SCIM client while reusing the shared resourceTypeUser. No issues here.

@aldevv aldevv force-pushed the business_plus_and_actions_fix branch from ddfdb18 to c5c1976 Compare December 6, 2025 04:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
pkg/connector/user.go (1)

37-56: Remove duplicate user_id assignments in user profile maps

profile["user_id"] is assigned twice in both scimUserResource and userResource (once with the raw ID, then again with fmt.Sprint). The second write is redundant because slack.User.ID/user.ID are already strings, and it just overwrites the same key.

You can simplify and avoid confusion by keeping a single assignment in each function:

@@ func (o *userResourceType) scimUserResource(...
-	profile["user_id"] = slackUser.ID
+	profile["user_id"] = slackUser.ID
@@
-	profile["user_id"] = fmt.Sprint(slackUser.ID)
@@ func userResource(...
-	profile["user_id"] = user.ID
+	profile["user_id"] = user.ID
@@
-	profile["user_id"] = fmt.Sprint(user.ID)

Also applies to: 100-118

🧹 Nitpick comments (4)
pkg/connector/client/request.go (2)

183-208: Clean centralization of SCIM request handling.

The new doScimRequest function provides a unified entry point for SCIM calls with optional JSON payload support. Note that this function passes target directly to doRequest (not &target), which is more correct than what getScim and patchScim do at lines 100 and 118. Consider unifying this behavior in a follow-up.


262-264: Consider preserving more context in SCIM error responses.

Using codes.Internal for all SCIM API errors loses semantic meaning. The SCIM API might return errors for various reasons (invalid input, not found, permission denied) that could map to more specific gRPC codes.

If you want to preserve more context, consider parsing the SCIM error schema more fully:

-		if detail, ok := errorResponse["detail"].(string); ok {
-			return &ratelimitData, uhttp.WrapErrors(codes.Internal, "SCIM API error", fmt.Errorf("%s", detail))
+		if detail, ok := errorResponse["detail"].(string); ok {
+			code := codes.Internal
+			if status, ok := errorResponse["status"].(string); ok && status == "404" {
+				code = codes.NotFound
+			}
+			return &ratelimitData, uhttp.WrapErrors(code, "SCIM API error", errors.New(detail))
		}

Also, fmt.Errorf("%s", detail) could be simplified to errors.New(detail) (requires adding "errors" import).

pkg/connector/user.go (2)

29-35: SCIM list path does N+1 Web API calls for user details

In listScimAPI, each SCIM user from ListIDPUsers is passed to scimUserResource, which then calls GetUserInfoContext once per user. On large workspaces this becomes N+1 pattern per page (1 SCIM call + up to 100 Web API calls), increasing latency and rate‑limit risk.

Consider restructuring so that you either:

  • Derive as much as possible directly from the SCIM UserResource (only falling back to Web API when essential), or
  • Batch Web API user retrieval (e.g., a single GetUsersContext for the team) and look up per‑user data from an in‑memory map instead of per‑user RPCs.

This keeps the current compatibility goal while being more scalable for Business+ tenants.

Also applies to: 231-255


241-247: Align SCIM error handling with centralized Slack error mapping

For SCIM listing errors you currently do:

return nil, &resource.SyncOpResults{Annotations: annos}, uhttp.WrapErrors(codes.Internal, "fetching SCIM users", err)

whereas the standard API path uses pkg.AnnotationsForError / pkg.WrapError to map Slack error strings to more specific gRPC codes (Unauthenticated, PermissionDenied, etc.).

If ListIDPUsers returns Slack‑style errors, consider reusing the same centralized helpers (or a SCIM‑aware variant) so SCIM failures surface with consistent and more precise gRPC codes, while still attaching ratelimitData via annos.WithRateLimiting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddfdb18 and c5c1976.

📒 Files selected for processing (2)
  • pkg/connector/client/request.go (6 hunks)
  • pkg/connector/user.go (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-28T17:48:53.644Z
Learnt from: mateoHernandez123
Repo: ConductorOne/baton-slack PR: 57
File: pkg/connector/roles.go:217-219
Timestamp: 2025-10-28T17:48:53.644Z
Learning: In the baton-slack connector, when client methods like SetWorkspaceRole(), RemoveUser(), or other enterprise/slack client calls already wrap errors with specific gRPC codes via handleError(), use fmt.Errorf with %w to preserve those codes while adding context. Only use uhttp.WrapErrors for errors generated directly in the function (e.g., validation, precondition checks). Double-wrapping with uhttp.WrapErrors would overwrite the specific codes (Unavailable, PermissionDenied, etc.) needed for proper retry logic and alerting.
<!-- [/add_learning]

Applied to files:

  • pkg/connector/client/request.go
🧬 Code graph analysis (1)
pkg/connector/user.go (3)
pkg/connector/client/slack.go (1)
  • Client (31-38)
pkg/connector/client/models.go (4)
  • UserResource (72-94)
  • Email (103-106)
  • Name (118-122)
  • User (33-59)
pkg/helpers.go (2)
  • WrapError (173-222)
  • AnnotationsForError (227-242)
🔇 Additional comments (4)
pkg/connector/client/request.go (4)

1-17: LGTM on package rename and imports.

The package rename from enterprise to client and the addition of the grpc/codes import align with the Business+ SCIM refactoring objectives.


153-154: Appropriate use of error wrapping.

Wrapping with uhttp.WrapErrors for request creation errors is correct here since this is a locally-generated error (not returned from a client method that already wraps errors). Based on learnings, this aligns with the convention.


173-178: Good defensive check before unmarshaling.

This correctly skips JSON unmarshaling for 204 No Content responses and empty bodies, preventing unnecessary unmarshal errors.


210-268: Verify if deleteScim could leverage doScimRequest.

The new doScimRequest helper centralizes SCIM calls, but deleteScim still uses direct request construction. This appears intentional due to the specialized response handling (NoContent check, error body parsing). If this pattern is expected for other SCIM operations, the current approach is reasonable.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
pkg/connector/user_group.go (1)

18-37: businessPlusClient field is declared but never used.

The businessPlusClient field is assigned in the struct but not referenced in List, Entitlements, or Grants methods. Either remove it or integrate it into the methods if Business+ functionality is intended.

pkg/connector/user.go (2)

40-53: Duplicate profile["user_id"] assignment.

profile["user_id"] is assigned on line 40 and overwritten on line 53. The second assignment using fmt.Sprint is redundant since slackUser.ID is already a string.

 profile["user_id"] = slackUser.ID
 // ... other assignments ...
-profile["user_id"] = fmt.Sprint(slackUser.ID)

103-116: Duplicate profile["user_id"] assignment.

Same issue as in scimUserResource - profile["user_id"] is set on line 103 and overwritten on line 116.

 profile["user_id"] = user.ID
 // ... other assignments ...
-profile["user_id"] = fmt.Sprint(user.ID)
🧹 Nitpick comments (3)
pkg/connector/user_group.go (1)

140-159: Consider batching user info fetches to reduce API calls.

The current implementation makes an individual GetUserInfoContext call for each group member, resulting in N+1 API calls. For groups with many members, this could be slow and may hit rate limits.

Consider using GetUsersInfoContext (batch API) if available, or caching user info from the parent workspace sync.

pkg/connector/workspace.go (1)

141-143: Silent fallback when businessPlusClient is nil may hide configuration issues.

When businessPlusClient is nil, the method silently returns empty grants. Consider logging a warning to help diagnose configuration issues when Business+ functionality is expected but the client is not configured.

 if o.businessPlusClient == nil {
+    l := ctxzap.Extract(ctx)
+    l.Debug("Business+ client not available, skipping workspace grants")
     return nil, &resources.SyncOpResults{}, nil
 }
pkg/connector/user.go (1)

27-34: Performance concern: N+1 API calls in SCIM user listing.

Each SCIM user triggers an additional GetUserInfoContext call to fetch the full profile. For workspaces with many users, this could be slow and may hit rate limits during pagination. Consider caching user info or batching requests if the Slack API supports it.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5c1976 and d82caf6.

📒 Files selected for processing (4)
  • pkg/connector/group.go (10 hunks)
  • pkg/connector/user.go (4 hunks)
  • pkg/connector/user_group.go (3 hunks)
  • pkg/connector/workspace.go (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-28T17:48:53.644Z
Learnt from: mateoHernandez123
Repo: ConductorOne/baton-slack PR: 57
File: pkg/connector/roles.go:217-219
Timestamp: 2025-10-28T17:48:53.644Z
Learning: In the baton-slack connector, when client methods like SetWorkspaceRole(), RemoveUser(), or other enterprise/slack client calls already wrap errors with specific gRPC codes via handleError(), use fmt.Errorf with %w to preserve those codes while adding context. Only use uhttp.WrapErrors for errors generated directly in the function (e.g., validation, precondition checks). Double-wrapping with uhttp.WrapErrors would overwrite the specific codes (Unavailable, PermissionDenied, etc.) needed for proper retry logic and alerting.
<!-- [/add_learning]

Applied to files:

  • pkg/connector/workspace.go
  • pkg/connector/user.go
  • pkg/connector/group.go
  • pkg/connector/user_group.go
🧬 Code graph analysis (3)
pkg/connector/workspace.go (3)
pkg/connector/client/slack.go (2)
  • Client (31-38)
  • SetWorkspaceNames (125-131)
pkg/helpers.go (2)
  • WrapError (173-222)
  • ParsePageToken (61-76)
pkg/connector/connector.go (1)
  • New (145-158)
pkg/connector/user.go (3)
pkg/connector/client/slack.go (1)
  • Client (31-38)
pkg/connector/client/models.go (3)
  • Email (103-106)
  • Name (118-122)
  • User (33-59)
pkg/helpers.go (2)
  • WrapError (173-222)
  • AnnotationsForError (227-242)
pkg/connector/user_group.go (3)
pkg/connector/client/slack.go (1)
  • Client (31-38)
pkg/connector/connector.go (1)
  • New (145-158)
pkg/helpers.go (1)
  • WrapError (173-222)
🔇 Additional comments (12)
pkg/connector/user_group.go (1)

62-96: LGTM!

The List method correctly handles the nil parent case, fetches user groups scoped by team ID, and wraps errors consistently with pkg.WrapError.

pkg/connector/workspace.go (4)

29-38: LGTM!

The builder correctly initializes the workspace resource type with both the standard Slack client and the Business+ client.


66-108: LGTM!

The List method correctly handles pagination, lists teams via the standard Slack API, creates workspace resources, and stores workspace names in the session for later reference.


176-177: LGTM!

The simplified grant logic correctly creates workspace membership grants without the Enterprise Grid role-based grants.


185-187: LGTM!

Good documentation explaining why Grant/Revoke are not implemented for Business+ plans.

pkg/connector/user.go (3)

179-200: LGTM!

The routing logic correctly prioritizes the SCIM API when the Business+ client is available and falls back to the standard API otherwise. Debug logging provides good visibility.


229-261: LGTM!

The SCIM-based listing correctly implements 1-indexed pagination, handles rate limiting annotations, and properly calculates the next page token.


202-227: The listStandardAPI method is correct as written. The Slack SDK's GetUsersContext automatically handles pagination internally and returns the aggregated list of all users, hiding the cursor loop from the caller. Unlike listScimAPI, which needs explicit pagination for sync operation page tokens, listStandardAPI relies on this built-in behavior and does not require manual pagination implementation.

Likely an incorrect or invalid review comment.

pkg/connector/group.go (4)

36-42: LGTM!

The builder correctly initializes the group resource type with the Business+ client and government environment flag.


105-109: LGTM!

Good defensive check with debug logging when the Business+ client is unavailable.


287-296: Same double-wrapping concern as in Grant.

The error from RemoveUserFromGroup is wrapped with codes.Internal, which may overwrite more specific error codes if the client method already wraps errors. Apply the same fix as suggested for Grant.


298-300: LGTM!

Good idempotent handling - the GrantAlreadyRevoked annotation correctly signals when the grant was already revoked without returning an error.

Comment on lines +239 to 247
ratelimitData, err := g.businessPlusClient.AddUserToGroup(
ctx,
entitlement.Resource.Id.Resource,
principal.Id.Resource,
)
outputAnnotations.WithRateLimiting(ratelimitData)
if err != nil {
return outputAnnotations, fmt.Errorf("failed to add user to IDP group during grant operation: %w", err)
return outputAnnotations, uhttp.WrapErrors(codes.Internal, "adding user to IDP group", err)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find businessPlusClient type and AddUserToGroup implementation
rg "businessPlusClient" pkg/connector/group.go -A 2 -B 2

Repository: ConductorOne/baton-slack

Length of output: 1719


🏁 Script executed:

# Search for businessPlusClient field definition in the file
rg "type.*Manager" pkg/connector/group.go -A 20

Repository: ConductorOne/baton-slack

Length of output: 50


🏁 Script executed:

# Find AddUserToGroup method implementation
rg "func.*AddUserToGroup" --type-list -g "*.go" -A 15

Repository: ConductorOne/baton-slack

Length of output: 457


🏁 Script executed:

# Find where client package is imported and AddUserToGroup is defined
rg "AddUserToGroup" --type go -A 10

Repository: ConductorOne/baton-slack

Length of output: 1345


🏁 Script executed:

# List client package files
fd "client" --type f -e go

Repository: ConductorOne/baton-slack

Length of output: 50


🏁 Script executed:

# Check imports in group.go to find client package location
head -30 pkg/connector/group.go | grep -E "import|client"

Repository: ConductorOne/baton-slack

Length of output: 171


🏁 Script executed:

# Get full AddUserToGroup implementation
sed -n '/^func (c \*Client) AddUserToGroup/,/^func [a-zA-Z]/p' pkg/connector/client/slack.go | head -60

Repository: ConductorOne/baton-slack

Length of output: 1456


🏁 Script executed:

# Check GetIDPGroup implementation to see error wrapping
rg "func.*GetIDPGroup" pkg/connector/client/slack.go -A 20

Repository: ConductorOne/baton-slack

Length of output: 482


🏁 Script executed:

# Search for handleError usage in client package
rg "handleError" pkg/connector/client/ -B 3 -A 3

Repository: ConductorOne/baton-slack

Length of output: 2609


🏁 Script executed:

# Check patchGroup implementation
rg "func.*patchGroup" pkg/connector/client/slack.go -A 20

Repository: ConductorOne/baton-slack

Length of output: 503


🏁 Script executed:

# Check getScim implementation to see if it wraps errors with gRPC codes
rg "func.*getScim" pkg/connector/client/slack.go -A 25

Repository: ConductorOne/baton-slack

Length of output: 50


🏁 Script executed:

# Search for doScimRequest implementation
rg "func.*doScimRequest" pkg/connector/client/ -A 30

Repository: ConductorOne/baton-slack

Length of output: 1544


🏁 Script executed:

# Search for gRPC codes being wrapped in client package
rg "codes\." pkg/connector/client/ -B 2 -A 2

Repository: ConductorOne/baton-slack

Length of output: 3210


🏁 Script executed:

# Get full doRequest implementation in request.go
rg "func.*doRequest" pkg/connector/client/request.go -A 80 | head -120

Repository: ConductorOne/baton-slack

Length of output: 1703


🏁 Script executed:

# Check if there's HTTP status to gRPC code mapping
rg "http.Status" pkg/connector/client/ -B 3 -A 3

Repository: ConductorOne/baton-slack

Length of output: 963


🏁 Script executed:

# Check for HTTP status code validation in request.go - look after Do() call
sed -n '/func (c \*Client) doRequest/,/^func/p' pkg/connector/client/request.go | tail -100

Repository: ConductorOne/baton-slack

Length of output: 1349


🏁 Script executed:

# Search for status code checking in the entire client package
rg "StatusCode" pkg/connector/client/ -B 5 -A 5

Repository: ConductorOne/baton-slack

Length of output: 1384


🏁 Script executed:

# Get the full deleteScim function and any other request handling functions
rg "func.*Scim" pkg/connector/client/request.go -A 35

Repository: ConductorOne/baton-slack

Length of output: 2184


🏁 Script executed:

# Check what uhttp.wrapper is and if it handles HTTP errors
rg "wrapper" pkg/connector/client/request.go | head -20

Repository: ConductorOne/baton-slack

Length of output: 209


🏁 Script executed:

# Find wrapper initialization and type
rg "wrapper" pkg/connector/client/slack.go -B 2 -A 2

Repository: ConductorOne/baton-slack

Length of output: 268


🏁 Script executed:

# Check imports in client files
head -50 pkg/connector/client/slack.go | grep -E "import|uhttp"

Repository: ConductorOne/baton-slack

Length of output: 158


🏁 Script executed:

# Search for uhttp.Do documentation or error handling
rg "uhttp" pkg/connector/ -l

Repository: ConductorOne/baton-slack

Length of output: 179


🏁 Script executed:

# Check if there's status code validation for errors anywhere in request.go
cat pkg/connector/client/request.go | grep -A 50 "c.wrapper.Do"

Repository: ConductorOne/baton-slack

Length of output: 2069


🏁 Script executed:

# Search for any HTTP error status handling
rg "http.Status" pkg/connector/client/request.go -B 5 -A 10

Repository: ConductorOne/baton-slack

Length of output: 910


🏁 Script executed:

# Get line numbers for doRequest and deleteScim to understand scope
rg "func.*doRequest\|func.*deleteScim" pkg/connector/client/request.go -n

Repository: ConductorOne/baton-slack

Length of output: 50


🏁 Script executed:

# Check if there are other request handler functions that validate status codes
rg "response.StatusCode" pkg/connector/client/request.go

Repository: ConductorOne/baton-slack

Length of output: 189


HTTP error status validation is missing in doRequest, causing SCIM API errors to be silently ignored.

The doRequest method in pkg/connector/client/request.go does not validate HTTP status codes for error responses (4xx, 5xx). Unlike deleteScim, which properly checks for error status codes and returns wrapped errors, doRequest only checks StatusCode != NoContent && len(bodyBytes) > 0 to unmarshal the response body as success. This means when the SCIM API returns a 4xx or 5xx error, doRequest returns nil error rather than reporting the failure. Consequently, AddUserToGroup and other methods relying on doRequest don't receive errors to wrap, and the operation appears to succeed when it actually failed.

Add HTTP status code validation in doRequest following the same pattern as deleteScim: check if response.StatusCode >= 400 and return an appropriate error before attempting to unmarshal the response body as target data.

@aldevv aldevv requested a review from a team December 9, 2025 14:14
@aldevv aldevv requested a review from a team December 9, 2025 19:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d82caf6 and 3db22a7.

📒 Files selected for processing (1)
  • pkg/connector/user.go (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-28T17:48:53.644Z
Learnt from: mateoHernandez123
Repo: ConductorOne/baton-slack PR: 57
File: pkg/connector/roles.go:217-219
Timestamp: 2025-10-28T17:48:53.644Z
Learning: In the baton-slack connector, when client methods like SetWorkspaceRole(), RemoveUser(), or other enterprise/slack client calls already wrap errors with specific gRPC codes via handleError(), use fmt.Errorf with %w to preserve those codes while adding context. Only use uhttp.WrapErrors for errors generated directly in the function (e.g., validation, precondition checks). Double-wrapping with uhttp.WrapErrors would overwrite the specific codes (Unavailable, PermissionDenied, etc.) needed for proper retry logic and alerting.
<!-- [/add_learning]

Applied to files:

  • pkg/connector/user.go
🔇 Additional comments (7)
pkg/connector/user.go (7)

3-15: LGTM!

The import changes appropriately support the new SCIM functionality and logging requirements.


17-21: LGTM!

The addition of businessPlusClient field properly supports the dual-client architecture.


92-151: LGTM! Duplicate assignment issue resolved.

The duplicate profile["user_id"] assignments flagged in previous reviews have been fixed. The profile map now correctly assigns each field only once.


177-198: LGTM!

The routing logic cleanly separates SCIM and standard API paths with appropriate debug logging.


200-225: LGTM!

The standard API listing logic is correct. Error from userResource is appropriately wrapped since it's a local function.

Note: The error handling for GetUsersContext at line 210 should be verified as part of the earlier comment about slack client error wrapping patterns.


261-269: LGTM!

The builder function correctly accepts and initializes both the standard and Business+ clients.


227-259: No changes needed—pkg.WrapError is the correct wrapper for businessPlusClient.ListIDPUsers.

The current code at line 242 using pkg.WrapError(err, "fetching SCIM users") is correct. Unlike the learning which cautions against double-wrapping when handleError() is already involved, ListIDPUsers does not use handleError(). Instead, pkg.WrapError is the proper wrapper here because it specifically maps Slack error strings to gRPC codes via MapSlackErrorToGRPCCode. Using fmt.Errorf with %w would lose the gRPC code mapping that pkg.WrapError provides, whereas pkg.WrapError preserves and properly assigns gRPC codes needed for retry logic and alerting.

Likely an incorrect or invalid review comment.

Comment on lines +27 to +33
func (o *userResourceType) scimUserResource(ctx context.Context, scimUser client.UserResource, parentResourceID *v2.ResourceId) (*v2.Resource, error) {
// NOTE: this is mainly to maintain compatibility with existing profile in non scim flow.
slackUser, err := o.client.GetUserInfoContext(ctx, scimUser.ID)
if err != nil {
wrappedErr := pkg.WrapError(err, fmt.Sprintf("fetching user info for SCIM user %s", scimUser.ID))
return nil, wrappedErr
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg "GetUserInfoContext" -B 2 -A 15

Repository: ConductorOne/baton-slack

Length of output: 3384


🏁 Script executed:

rg "handleError" -B 2 -A 10

Repository: ConductorOne/baton-slack

Length of output: 4641


🏁 Script executed:

rg "pkg\.WrapError|uhttp\.WrapError" -B 2 -A 2

Repository: ConductorOne/baton-slack

Length of output: 14383


🏁 Script executed:

fd "client" -type f -name "*.go" | head -20

Repository: ConductorOne/baton-slack

Length of output: 238


🏁 Script executed:

rg "func WrapError" -B 2 -A 10

Repository: ConductorOne/baton-slack

Length of output: 745


🏁 Script executed:

rg "MapSlackErrorToGRPCCode" -B 2 -A 20

Repository: ConductorOne/baton-slack

Length of output: 4782


Fix error handling: use fmt.Errorf with %w instead of pkg.WrapError.

The o.client.GetUserInfoContext() call already wraps errors with gRPC codes via handleError(). Using pkg.WrapError causes double-wrapping and overwrites those codes. Instead, wrap with fmt.Errorf to preserve the gRPC context:

slackUser, err := o.client.GetUserInfoContext(ctx, scimUser.ID)
if err != nil {
	return nil, fmt.Errorf("fetching user info for SCIM user %s: %w", scimUser.ID, err)
}

Address the N+1 API call pattern for SCIM users.

For every SCIM user, this method makes an additional GetUserInfoContext API call. With hundreds or thousands of users, this creates significant performance degradation and rate limit risk. Consider whether SCIM user data can provide the needed fields directly, or if a batch API exists to fetch multiple user profiles at once.


bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
logBody(ctx, response)

Choose a reason for hiding this comment

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

I would verify if response is nill before logging

Choose a reason for hiding this comment

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

logBody tries to read from the body after io.ReadAll has already consumed it. Maybe you could refactor logBody to accept []byte and pass bodyBytes directly, or log bodyBytes without using logBody.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

uhttp.WithRatelimitData(&ratelimitData),
)
if err != nil {
logBody(ctx, response)

Choose a reason for hiding this comment

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

same here


bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
logBody(ctx, response)

Choose a reason for hiding this comment

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

same here

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3db22a7 and a555432.

📒 Files selected for processing (1)
  • pkg/connector/client/helpers.go (2 hunks)
🔇 Additional comments (2)
pkg/connector/client/helpers.go (2)

1-1: Package rename looks consistent with location

package client matches the pkg/connector/client path and keeps helpers colocated with the HTTP client; no concerns here.


14-22: Good defensive checks on nil response/Body

The extra guards for response == nil and response.Body == nil avoid panics in logging and give clearer error messages. This is a solid robustness improvement.

Comment on lines 24 to +30
body := make([]byte, 512)
_, err := bodyCloser.Read(body)
_, err := response.Body.Read(body)
if err != nil {
l.Error("error reading response body", zap.Error(err))
return
}
l.Info("response body: ", zap.String("body", string(body)))
l.Info("response body", zap.String("body", string(body)))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the actual bytes read and consider a bounded io.ReadAll

Right now you ignore the byte count from Read and always log the full 512‑byte buffer. That can introduce trailing \x00 bytes into logs and makes partial reads harder to reason about. It’s also a bit clunky compared to using io.LimitReader.

A small refactor would improve correctness and clarity:

-import (
-	"context"
-	"net/http"
-
-	"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
-	"go.uber.org/zap"
-)
+import (
+	"context"
+	"io"
+	"net/http"
+
+	"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
+	"go.uber.org/zap"
+)

 func logBody(ctx context.Context, response *http.Response) {
@@
-	body := make([]byte, 512)
-	_, err := response.Body.Read(body)
-	if err != nil {
-		l.Error("error reading response body", zap.Error(err))
-		return
-	}
-	l.Info("response body", zap.String("body", string(body)))
+	// Limit how much we read for logging while respecting the actual length read.
+	body, err := io.ReadAll(io.LimitReader(response.Body, 512))
+	if err != nil {
+		l.Error("error reading response body", zap.Error(err))
+		return
+	}
+	l.Info("response body", zap.ByteString("body", body))
 }

This keeps the 512‑byte cap, logs exactly what was read, and avoids zero‑padding artifacts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
body := make([]byte, 512)
_, err := bodyCloser.Read(body)
_, err := response.Body.Read(body)
if err != nil {
l.Error("error reading response body", zap.Error(err))
return
}
l.Info("response body: ", zap.String("body", string(body)))
l.Info("response body", zap.String("body", string(body)))
// Limit how much we read for logging while respecting the actual length read.
body, err := io.ReadAll(io.LimitReader(response.Body, 512))
if err != nil {
l.Error("error reading response body", zap.Error(err))
return
}
l.Info("response body", zap.ByteString("body", body))
🤖 Prompt for AI Agents
In pkg/connector/client/helpers.go around lines 24 to 30, the code ignores the
byte count returned by response.Body.Read and always logs the full 512‑byte
buffer (producing trailing \x00), and it should instead log only the bytes
actually read and cap reads to 512 bytes: replace the current Read call with
either io.ReadAll(io.LimitReader(response.Body, 512)) and log the returned slice
as a string (handling errors and io.EOF appropriately), or keep the fixed buffer
but use n, err := response.Body.Read(body); if err != nil && err != io.EOF { log
error; return } then log string(body[:n]) so only the actual bytes read are
logged and zero‑padding is avoided.

@laurenleach
Copy link

@aldevv do we have a migration plan for existing customers that are already using slack enterprise grid? (with this repo, not with new repo)

@aldevv
Copy link
Contributor Author

aldevv commented Dec 9, 2025

@aldevv do we have a migration plan for existing customers that are already using slack enterprise grid? (with this repo, not with new repo)

not as far as I know

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/connector/client/request.go (1)

182-207: The doScimRequest helper is actively used, but the refactoring observation remains valid.

doScimRequest is called from pkg/connector/client/slack.go:486 with HTTP PATCH operations and a payload. However, the patchScim method (line 105) implements nearly identical logic: both execute PATCH requests with bearer token authentication and JSON payload handling. This represents a consistency gap where patchScim could be refactored to delegate to doScimRequest to avoid duplication. The getScim method cannot use this helper due to its query parameter support, and deleteScim uses a different pattern with custom logging and direct wrapper calls.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a555432 and a3d8409.

📒 Files selected for processing (1)
  • pkg/connector/client/request.go (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-28T17:48:53.644Z
Learnt from: mateoHernandez123
Repo: ConductorOne/baton-slack PR: 57
File: pkg/connector/roles.go:217-219
Timestamp: 2025-10-28T17:48:53.644Z
Learning: In the baton-slack connector, when client methods like SetWorkspaceRole(), RemoveUser(), or other enterprise/slack client calls already wrap errors with specific gRPC codes via handleError(), use fmt.Errorf with %w to preserve those codes while adding context. Only use uhttp.WrapErrors for errors generated directly in the function (e.g., validation, precondition checks). Double-wrapping with uhttp.WrapErrors would overwrite the specific codes (Unavailable, PermissionDenied, etc.) needed for proper retry logic and alerting.
<!-- [/add_learning]

Applied to files:

  • pkg/connector/client/request.go
🧬 Code graph analysis (1)
pkg/connector/client/request.go (1)
pkg/connector/client/slack.go (1)
  • Client (31-38)
🔇 Additional comments (4)
pkg/connector/client/request.go (4)

1-1: LGTM!

The package rename from enterprise to client and the addition of the gRPC codes import support the refactoring to Business+ SCIM.

Also applies to: 16-16


153-153: LGTM!

The error wrapping for request creation and body reading uses uhttp.WrapErrors appropriately for errors generated directly in this function. Based on learnings, this is the correct pattern for internal operation errors (vs. errors from wrapped client methods).

Also applies to: 169-169


172-177: LGTM!

The defensive check for StatusNoContent and non-empty body before unmarshaling prevents unnecessary unmarshal attempts on empty responses. The error wrapping is consistent with the pattern used throughout the file.


231-231: LGTM!

The error wrapping for SCIM delete operations appropriately uses uhttp.WrapErrors for internal operation errors with descriptive context. The pattern is consistent with the learning that internal operations should be wrapped this way.

Also applies to: 252-252, 259-259

}
if detail, ok := errorResponse["detail"].(string); ok {
return &ratelimitData, fmt.Errorf("SCIM API error: %s", detail)
return &ratelimitData, uhttp.WrapErrors(codes.Internal, "SCIM API error", fmt.Errorf("%s", detail))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Map HTTP status to appropriate gRPC code instead of always using codes.Internal.

The code unconditionally wraps SCIM API errors with codes.Internal, but the actual HTTP status could indicate a client error (4xx) or server error (5xx). Using the wrong code affects retry logic and alerting.

Inspect response.StatusCode to map to the appropriate gRPC code:

  • 400-level errors → codes.InvalidArgument, codes.NotFound, codes.AlreadyExists, etc.
  • 500-level errors → codes.Internal, codes.Unavailable, etc.

Apply this pattern to map status codes appropriately:

+	// Map HTTP status to appropriate gRPC code
+	code := codes.Internal
+	if response.StatusCode >= 400 && response.StatusCode < 500 {
+		code = codes.InvalidArgument // or more specific based on status
+	}
+
 	if detail, ok := errorResponse["detail"].(string); ok {
-		return &ratelimitData, uhttp.WrapErrors(codes.Internal, "SCIM API error", fmt.Errorf("%s", detail))
+		return &ratelimitData, uhttp.WrapErrors(code, "SCIM API error", fmt.Errorf("%s", detail))
 	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pkg/connector/client/request.go around line 262, the error returned always
uses codes.Internal regardless of response.StatusCode; update the code to
inspect response.StatusCode and map HTTP ranges/statuses to appropriate gRPC
codes before wrapping the error. For 4xx responses map common codes (e.g.,
400→codes.InvalidArgument, 401→codes.PermissionDenied or codes.Unauthenticated
as appropriate, 403→codes.PermissionDenied, 404→codes.NotFound,
409→codes.AlreadyExists, 429→codes.ResourceExhausted) and for 5xx responses map
to server-side codes (e.g., 500→codes.Internal, 503→codes.Unavailable), then
call uhttp.WrapErrors with the chosen gRPC code and include the HTTP status and
response detail in the error message to preserve context.

@aldevv aldevv requested a review from a team December 9, 2025 21:31
"sso-enabled",
field.WithDisplayName("SSO Enabled"),
field.WithDescription("Flag indicating that the SSO has been configured for Enterprise Grid Organization. Enables usage of SCIM API"),
field.WithDescription("Deprecated: Enterprise Grid features moved to baton-slack-enterprise. This connector focuses on Business+ plans."),

Choose a reason for hiding this comment

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

I think we should just remove the field, or at least use WithExportTarget(ExportTargetNone) so it doesn't show up in the UI

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done, removed

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
pkg/config/config.go (1)

17-22: BusinessPlus token field looks consistent; consider clarifying when it’s required

The rename to BusinessPlusTokenField and the "business-plus-token" key look consistent with the shift away from the enterprise-specific field, and keeping it secret-only (not required) matches the idea that it’s only needed for certain plans.

You might optionally tweak the description to make the usage a bit more self‑service, e.g., explicitly call out that it’s only needed for Business+ or Enterprise Grid tenants and which capabilities it unlocks (Admin/SCIM vs. standard bot API), so non‑Business+ users know they can safely leave it empty.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3d8409 and 855c277.

📒 Files selected for processing (1)
  • pkg/config/config.go (2 hunks)
🔇 Additional comments (1)
pkg/config/config.go (1)

35-47: Gov env dependency on BusinessPlus token wiring looks correct

Adding BusinessPlusTokenField into ConfigurationFields and updating FieldRelationships so GovEnvironmentField depends on BusinessPlusTokenField preserves the previous constraint pattern (Gov env implies an admin/SCIM-style token), just with the new field.

No issues from a correctness standpoint; the relationship and the updated comment about Gov instances being Business+ or Enterprise Grid read coherently with the rest of the PR.

@aldevv aldevv merged commit 4e9c5ab into main Dec 11, 2025
6 of 7 checks passed
@aldevv aldevv deleted the business_plus_and_actions_fix branch December 11, 2025 16:30
- name: Install Go
uses: actions/setup-go@v5
- name: Grant/revoke
uses: ConductorOne/github-workflows/actions/sync-test@v2
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's a newer version of the sync test now.

field.WithDisplayName("Enterprise Token"),
field.WithDescription("The Slack user oauth token used to connect to the Slack Enterprise Grid Admin API"),
BusinessPlusTokenField = field.StringField(
"business-plus-token",
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just keep the old name? I'm sure Slack will rebrand this product in the future, and we don't want to break backward compatibility every time they do that.

AccessTokenField,
EnterpriseTokenField,
SSOEnabledField,
BusinessPlusTokenField,
Copy link
Contributor

Choose a reason for hiding this comment

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

If SSOEnabledField is removed, I don't think it'll show up anywhere in --help, and I think if someone specifies it, the connector will error with "unknown flag: --sso-enabled".

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.

6 participants