Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions pkg/connector/client/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,6 @@ func NewClient(
}, nil
}

// SetWorkspaceNames stores workspace names in the session store.
func SetWorkspaceNames(ctx context.Context, ss sessions.SessionStore, workspaces []slack.Team) error {
workspaceMap := make(map[string]string)
for _, workspace := range workspaces {
workspaceMap[workspace.ID] = workspace.Name
}
return session.SetManyJSON(ctx, ss, workspaceMap, workspaceNameNamespace)
}

// GetUserInfo returns the user info for the given user ID.
func (c *Client) GetUserInfo(
ctx context.Context,
Expand Down Expand Up @@ -511,3 +502,39 @@ func (c *Client) EnableUser(

return ratelimitData, nil
}

func SetWorkspaceNames(ctx context.Context, ss sessions.SessionStore, workspaces []slack.Team) error {
workspaceMap := make(map[string]string)
for _, workspace := range workspaces {
workspaceMap[workspace.ID] = workspace.Name
}
return session.SetManyJSON(ctx, ss, workspaceMap, workspaceNameNamespace)
}

// GetWorkspaceNames retrieves workspace names for the given IDs from the session store.
func GetWorkspaceNames(ctx context.Context, ss sessions.SessionStore, workspaceIDs []string) (map[string]string, []string, error) {
validIDs := make([]string, 0, len(workspaceIDs))
for _, id := range workspaceIDs {
if id != "" {
validIDs = append(validIDs, id)
}
}

if len(validIDs) == 0 {
return make(map[string]string), []string{}, nil
}

found, err := session.GetManyJSON[string](ctx, ss, validIDs, workspaceNameNamespace)
if err != nil {
return nil, nil, err
}

missing := make([]string, 0)
for _, id := range validIDs {
if _, exists := found[id]; !exists {
missing = append(missing, id)
}
}

return found, missing, nil
}
1 change: 1 addition & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,6 @@ func (s *Slack) ResourceSyncers(ctx context.Context) []connectorbuilder.Resource
workspaceBuilder(s.client, s.businessPlusClient),
userGroupBuilder(s.client, s.businessPlusClient),
groupBuilder(s.businessPlusClient, s.govEnv),
workspaceRoleBuilder(s.businessPlusClient),
}
}
9 changes: 9 additions & 0 deletions pkg/connector/resource_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,13 @@ var (
v2.ResourceType_TRAIT_GROUP,
},
}

resourceTypeWorkspaceRole = &v2.ResourceType{
Id: "workspaceRole",
DisplayName: "Workspace Role",
Annotations: annotations.New(&v2.SkipGrants{}),
Traits: []v2.ResourceType_Trait{
v2.ResourceType_TRAIT_ROLE,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Annotations: annotations.New(&v2.SkipGrants{}), since grants is a noop?

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

}
)
83 changes: 77 additions & 6 deletions pkg/connector/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
resources "github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/conductorone/baton-slack/pkg"
"github.com/conductorone/baton-slack/pkg/connector/client"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/slack-go/slack"
)

Expand Down Expand Up @@ -59,6 +60,7 @@ func workspaceResource(
resources.WithAnnotation(
&v2.ChildResourceType{ResourceTypeId: resourceTypeUser.Id},
&v2.ChildResourceType{ResourceTypeId: resourceTypeUserGroup.Id},
&v2.ChildResourceType{ResourceTypeId: resourceTypeWorkspaceRole.Id},
),
)
}
Expand All @@ -83,6 +85,11 @@ func (o *workspaceResourceType) List(
return nil, nil, client.WrapError(err, "error listing teams")
}

err = client.SetWorkspaceNames(ctx, attrs.Session, workspaces)
if err != nil {
return nil, nil, fmt.Errorf("storing workspace names in session: %w", err)
}

rv := make([]*v2.Resource, 0, len(workspaces))
for _, ws := range workspaces {
resource, err := workspaceResource(ctx, ws, parentID)
Expand All @@ -92,11 +99,6 @@ func (o *workspaceResourceType) List(
rv = append(rv, resource)
}

err = client.SetWorkspaceNames(ctx, attrs.Session, workspaces)
if err != nil {
return nil, nil, fmt.Errorf("storing workspace names in session: %w", err)
}

pageToken, err := bag.NextToken(nextCursor)
if err != nil {
return nil, nil, fmt.Errorf("creating next page token: %w", err)
Expand Down Expand Up @@ -132,12 +134,15 @@ func (o *workspaceResourceType) Entitlements(
}, &resources.SyncOpResults{}, nil
}

// sets workspace memberships and workspace roles.
func (o *workspaceResourceType) Grants(
ctx context.Context,
resource *v2.Resource,
attrs resources.SyncOpAttrs,
) ([]*v2.Grant, *resources.SyncOpResults, error) {
l := ctxzap.Extract(ctx)
if o.businessPlusClient == nil {
l.Debug("Business+ client not available, skipping workspace grants")
return nil, &resources.SyncOpResults{}, nil
}

Expand Down Expand Up @@ -167,12 +172,78 @@ func (o *workspaceResourceType) Grants(
if user.IsStranger {
continue
}
if user.Deleted {
continue
}
userID, err := resources.NewResourceID(resourceTypeUser, user.ID)
if err != nil {
return nil, nil, fmt.Errorf("creating user resource ID: %w", err)
}

// Only create workspace membership grants (no role-based grants)
if user.IsPrimaryOwner {
rr, err := roleResource(ctx, PrimaryOwnerRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating primary owner role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
}

if user.IsOwner {
rr, err := roleResource(ctx, OwnerRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating owner role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
}

if user.IsAdmin {
rr, err := roleResource(ctx, AdminRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating admin role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
}

if user.IsRestricted {
if user.IsUltraRestricted {
rr, err := roleResource(ctx, SingleChannelGuestRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating single channel guest role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
} else {
rr, err := roleResource(ctx, MultiChannelGuestRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating multi channel guest role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
}
}

if user.IsInvitedUser {
rr, err := roleResource(ctx, InvitedMemberRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating invited member role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
}

if !user.IsRestricted && !user.IsUltraRestricted && !user.IsInvitedUser && !user.IsBot && !user.Deleted {
rr, err := roleResource(ctx, MemberRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating member role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
}

if user.IsBot {
rr, err := roleResource(ctx, BotRoleID, resource.Id)
if err != nil {
return nil, nil, fmt.Errorf("creating bot role resource: %w", err)
}
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
}

rv = append(rv, grant.NewGrant(resource, memberEntitlement, userID))
}

Expand Down
164 changes: 164 additions & 0 deletions pkg/connector/workspaceRoles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package connector

import (
"context"
"fmt"
"maps"
"slices"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/types/entitlement"
resources "github.com/conductorone/baton-sdk/pkg/types/resource"

"github.com/conductorone/baton-slack/pkg"
"github.com/conductorone/baton-slack/pkg/connector/client"
)

const (
PrimaryOwnerRoleID = "primary_owner"
OwnerRoleID = "owner"
AdminRoleID = "admin"
MultiChannelGuestRoleID = "multi_channel_guest"
SingleChannelGuestRoleID = "single_channel_guest"
InvitedMemberRoleID = "invited_member"
BotRoleID = "bot"
MemberRoleID = "member"
RoleAssignmentEntitlement = "assigned"
)

var roles = map[string]string{
PrimaryOwnerRoleID: "Primary Owner",
OwnerRoleID: "Owner",
AdminRoleID: "Admin",
MultiChannelGuestRoleID: "Multi Channel Guest",
SingleChannelGuestRoleID: "Single Channel Guest",
InvitedMemberRoleID: "Invited member",
BotRoleID: "Bot",
MemberRoleID: "Member",
}

type workspaceRoleType struct {
resourceType *v2.ResourceType
businessPlusClient *client.Client
}

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

func workspaceRoleBuilder(businessPlusClient *client.Client) *workspaceRoleType {
return &workspaceRoleType{
resourceType: resourceTypeWorkspaceRole,
businessPlusClient: businessPlusClient,
}
}

func roleResource(
_ context.Context,
roleID string,
parentResourceID *v2.ResourceId,
) (*v2.Resource, error) {
roleName, ok := roles[roleID]
if !ok {
return nil, fmt.Errorf("invalid roleID: %s", roleID)
}

roleId := fmt.Sprintf("%s:%s", parentResourceID.Resource, roleID)

r, err := resources.NewRoleResource(
roleName,
resourceTypeWorkspaceRole,
roleId,
nil,
resources.WithParentResourceID(parentResourceID))
if err != nil {
return nil, err
}

return r, nil
}

func (o *workspaceRoleType) List(
ctx context.Context,
parentResourceID *v2.ResourceId,
_ resources.SyncOpAttrs,
) (
[]*v2.Resource,
*resources.SyncOpResults,
error,
) {
if parentResourceID == nil {
return nil, &resources.SyncOpResults{}, nil
}

output, err := pkg.MakeResourceList(
ctx,
slices.Collect(maps.Keys(roles)),
parentResourceID,
roleResource,
)
if err != nil {
return nil, nil, err
}
return output, &resources.SyncOpResults{}, nil
}

func (o *workspaceRoleType) Entitlements(
ctx context.Context,
resource *v2.Resource,
attrs resources.SyncOpAttrs,
) (
[]*v2.Entitlement,
*resources.SyncOpResults,
error,
) {
found, missing, err := client.GetWorkspaceNames(ctx, attrs.Session, []string{resource.ParentResourceId.Resource})
if err != nil {
return nil, nil, fmt.Errorf("error getting workspace name for workspace id %s: %w", resource.ParentResourceId.Resource, err)
}
workspaceName, exists := found[resource.ParentResourceId.Resource]
if !exists {
return nil, nil, fmt.Errorf("workspace not found in cache: %s (missing: %v)", resource.ParentResourceId.Resource, missing)
}
return []*v2.Entitlement{
entitlement.NewAssignmentEntitlement(
resource,
RoleAssignmentEntitlement,
entitlement.WithGrantableTo(resourceTypeUser),
entitlement.WithDescription(
fmt.Sprintf(
"Has the %s role in the Slack %s workspace",
resource.DisplayName,
workspaceName,
),
),
entitlement.WithDisplayName(
fmt.Sprintf(
"%s workspace %s role",
workspaceName,
resource.DisplayName,
),
),
),
},
&resources.SyncOpResults{},
nil
}

// Grants would normally return the grants for each role resource. Due to how
// the Slack API works, it is more efficient to emit these roles while listing
// grants for each individual user. Instead of having to list users for each
// role we can divine which roles a user should be granted when calculating
// their grants.
// TLDR: workspaceRoles are set in the workspace.go's Grants method.
func (o *workspaceRoleType) Grants(
_ context.Context,
_ *v2.Resource,
_ resources.SyncOpAttrs,
) (
[]*v2.Grant,
*resources.SyncOpResults,
error,
) {
return nil, &resources.SyncOpResults{}, nil
}