Skip to content

Commit 203fdb5

Browse files
authored
[BB-1925] Add workspace role (#69)
* working workspaceRole * add error messages * fix lint * fix unused var and method receiver
1 parent 735b03f commit 203fdb5

File tree

5 files changed

+287
-15
lines changed

5 files changed

+287
-15
lines changed

pkg/connector/client/slack.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,6 @@ func NewClient(
6969
}, nil
7070
}
7171

72-
// SetWorkspaceNames stores workspace names in the session store.
73-
func SetWorkspaceNames(ctx context.Context, ss sessions.SessionStore, workspaces []slack.Team) error {
74-
workspaceMap := make(map[string]string)
75-
for _, workspace := range workspaces {
76-
workspaceMap[workspace.ID] = workspace.Name
77-
}
78-
return session.SetManyJSON(ctx, ss, workspaceMap, workspaceNameNamespace)
79-
}
80-
8172
// GetUserInfo returns the user info for the given user ID.
8273
func (c *Client) GetUserInfo(
8374
ctx context.Context,
@@ -511,3 +502,39 @@ func (c *Client) EnableUser(
511502

512503
return ratelimitData, nil
513504
}
505+
506+
func SetWorkspaceNames(ctx context.Context, ss sessions.SessionStore, workspaces []slack.Team) error {
507+
workspaceMap := make(map[string]string)
508+
for _, workspace := range workspaces {
509+
workspaceMap[workspace.ID] = workspace.Name
510+
}
511+
return session.SetManyJSON(ctx, ss, workspaceMap, workspaceNameNamespace)
512+
}
513+
514+
// GetWorkspaceNames retrieves workspace names for the given IDs from the session store.
515+
func GetWorkspaceNames(ctx context.Context, ss sessions.SessionStore, workspaceIDs []string) (map[string]string, []string, error) {
516+
validIDs := make([]string, 0, len(workspaceIDs))
517+
for _, id := range workspaceIDs {
518+
if id != "" {
519+
validIDs = append(validIDs, id)
520+
}
521+
}
522+
523+
if len(validIDs) == 0 {
524+
return make(map[string]string), []string{}, nil
525+
}
526+
527+
found, err := session.GetManyJSON[string](ctx, ss, validIDs, workspaceNameNamespace)
528+
if err != nil {
529+
return nil, nil, err
530+
}
531+
532+
missing := make([]string, 0)
533+
for _, id := range validIDs {
534+
if _, exists := found[id]; !exists {
535+
missing = append(missing, id)
536+
}
537+
}
538+
539+
return found, missing, nil
540+
}

pkg/connector/connector.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,6 @@ func (s *Slack) ResourceSyncers(ctx context.Context) []connectorbuilder.Resource
162162
workspaceBuilder(s.client, s.businessPlusClient),
163163
userGroupBuilder(s.client, s.businessPlusClient),
164164
groupBuilder(s.businessPlusClient, s.govEnv),
165+
workspaceRoleBuilder(s.businessPlusClient),
165166
}
166167
}

pkg/connector/resource_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,13 @@ var (
3535
v2.ResourceType_TRAIT_GROUP,
3636
},
3737
}
38+
39+
resourceTypeWorkspaceRole = &v2.ResourceType{
40+
Id: "workspaceRole",
41+
DisplayName: "Workspace Role",
42+
Annotations: annotations.New(&v2.SkipGrants{}),
43+
Traits: []v2.ResourceType_Trait{
44+
v2.ResourceType_TRAIT_ROLE,
45+
},
46+
}
3847
)

pkg/connector/workspace.go

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
resources "github.com/conductorone/baton-sdk/pkg/types/resource"
1212
"github.com/conductorone/baton-slack/pkg"
1313
"github.com/conductorone/baton-slack/pkg/connector/client"
14+
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
1415
"github.com/slack-go/slack"
1516
)
1617

@@ -59,6 +60,7 @@ func workspaceResource(
5960
resources.WithAnnotation(
6061
&v2.ChildResourceType{ResourceTypeId: resourceTypeUser.Id},
6162
&v2.ChildResourceType{ResourceTypeId: resourceTypeUserGroup.Id},
63+
&v2.ChildResourceType{ResourceTypeId: resourceTypeWorkspaceRole.Id},
6264
),
6365
)
6466
}
@@ -83,6 +85,11 @@ func (o *workspaceResourceType) List(
8385
return nil, nil, client.WrapError(err, "error listing teams")
8486
}
8587

88+
err = client.SetWorkspaceNames(ctx, attrs.Session, workspaces)
89+
if err != nil {
90+
return nil, nil, fmt.Errorf("storing workspace names in session: %w", err)
91+
}
92+
8693
rv := make([]*v2.Resource, 0, len(workspaces))
8794
for _, ws := range workspaces {
8895
resource, err := workspaceResource(ctx, ws, parentID)
@@ -92,11 +99,6 @@ func (o *workspaceResourceType) List(
9299
rv = append(rv, resource)
93100
}
94101

95-
err = client.SetWorkspaceNames(ctx, attrs.Session, workspaces)
96-
if err != nil {
97-
return nil, nil, fmt.Errorf("storing workspace names in session: %w", err)
98-
}
99-
100102
pageToken, err := bag.NextToken(nextCursor)
101103
if err != nil {
102104
return nil, nil, fmt.Errorf("creating next page token: %w", err)
@@ -132,12 +134,15 @@ func (o *workspaceResourceType) Entitlements(
132134
}, &resources.SyncOpResults{}, nil
133135
}
134136

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

@@ -167,12 +172,78 @@ func (o *workspaceResourceType) Grants(
167172
if user.IsStranger {
168173
continue
169174
}
175+
if user.Deleted {
176+
continue
177+
}
170178
userID, err := resources.NewResourceID(resourceTypeUser, user.ID)
171179
if err != nil {
172180
return nil, nil, fmt.Errorf("creating user resource ID: %w", err)
173181
}
174182

175-
// Only create workspace membership grants (no role-based grants)
183+
if user.IsPrimaryOwner {
184+
rr, err := roleResource(ctx, PrimaryOwnerRoleID, resource.Id)
185+
if err != nil {
186+
return nil, nil, fmt.Errorf("creating primary owner role resource: %w", err)
187+
}
188+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
189+
}
190+
191+
if user.IsOwner {
192+
rr, err := roleResource(ctx, OwnerRoleID, resource.Id)
193+
if err != nil {
194+
return nil, nil, fmt.Errorf("creating owner role resource: %w", err)
195+
}
196+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
197+
}
198+
199+
if user.IsAdmin {
200+
rr, err := roleResource(ctx, AdminRoleID, resource.Id)
201+
if err != nil {
202+
return nil, nil, fmt.Errorf("creating admin role resource: %w", err)
203+
}
204+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
205+
}
206+
207+
if user.IsRestricted {
208+
if user.IsUltraRestricted {
209+
rr, err := roleResource(ctx, SingleChannelGuestRoleID, resource.Id)
210+
if err != nil {
211+
return nil, nil, fmt.Errorf("creating single channel guest role resource: %w", err)
212+
}
213+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
214+
} else {
215+
rr, err := roleResource(ctx, MultiChannelGuestRoleID, resource.Id)
216+
if err != nil {
217+
return nil, nil, fmt.Errorf("creating multi channel guest role resource: %w", err)
218+
}
219+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
220+
}
221+
}
222+
223+
if user.IsInvitedUser {
224+
rr, err := roleResource(ctx, InvitedMemberRoleID, resource.Id)
225+
if err != nil {
226+
return nil, nil, fmt.Errorf("creating invited member role resource: %w", err)
227+
}
228+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
229+
}
230+
231+
if !user.IsRestricted && !user.IsUltraRestricted && !user.IsInvitedUser && !user.IsBot && !user.Deleted {
232+
rr, err := roleResource(ctx, MemberRoleID, resource.Id)
233+
if err != nil {
234+
return nil, nil, fmt.Errorf("creating member role resource: %w", err)
235+
}
236+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
237+
}
238+
239+
if user.IsBot {
240+
rr, err := roleResource(ctx, BotRoleID, resource.Id)
241+
if err != nil {
242+
return nil, nil, fmt.Errorf("creating bot role resource: %w", err)
243+
}
244+
rv = append(rv, grant.NewGrant(rr, RoleAssignmentEntitlement, userID))
245+
}
246+
176247
rv = append(rv, grant.NewGrant(resource, memberEntitlement, userID))
177248
}
178249

pkg/connector/workspaceRoles.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package connector
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"maps"
7+
"slices"
8+
9+
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
10+
"github.com/conductorone/baton-sdk/pkg/types/entitlement"
11+
resources "github.com/conductorone/baton-sdk/pkg/types/resource"
12+
13+
"github.com/conductorone/baton-slack/pkg"
14+
"github.com/conductorone/baton-slack/pkg/connector/client"
15+
)
16+
17+
const (
18+
PrimaryOwnerRoleID = "primary_owner"
19+
OwnerRoleID = "owner"
20+
AdminRoleID = "admin"
21+
MultiChannelGuestRoleID = "multi_channel_guest"
22+
SingleChannelGuestRoleID = "single_channel_guest"
23+
InvitedMemberRoleID = "invited_member"
24+
BotRoleID = "bot"
25+
MemberRoleID = "member"
26+
RoleAssignmentEntitlement = "assigned"
27+
)
28+
29+
var roles = map[string]string{
30+
PrimaryOwnerRoleID: "Primary Owner",
31+
OwnerRoleID: "Owner",
32+
AdminRoleID: "Admin",
33+
MultiChannelGuestRoleID: "Multi Channel Guest",
34+
SingleChannelGuestRoleID: "Single Channel Guest",
35+
InvitedMemberRoleID: "Invited member",
36+
BotRoleID: "Bot",
37+
MemberRoleID: "Member",
38+
}
39+
40+
type workspaceRoleType struct {
41+
resourceType *v2.ResourceType
42+
businessPlusClient *client.Client
43+
}
44+
45+
func (o *workspaceRoleType) ResourceType(_ context.Context) *v2.ResourceType {
46+
return o.resourceType
47+
}
48+
49+
func workspaceRoleBuilder(businessPlusClient *client.Client) *workspaceRoleType {
50+
return &workspaceRoleType{
51+
resourceType: resourceTypeWorkspaceRole,
52+
businessPlusClient: businessPlusClient,
53+
}
54+
}
55+
56+
func roleResource(
57+
_ context.Context,
58+
roleID string,
59+
parentResourceID *v2.ResourceId,
60+
) (*v2.Resource, error) {
61+
roleName, ok := roles[roleID]
62+
if !ok {
63+
return nil, fmt.Errorf("invalid roleID: %s", roleID)
64+
}
65+
66+
roleId := fmt.Sprintf("%s:%s", parentResourceID.Resource, roleID)
67+
68+
r, err := resources.NewRoleResource(
69+
roleName,
70+
resourceTypeWorkspaceRole,
71+
roleId,
72+
nil,
73+
resources.WithParentResourceID(parentResourceID))
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return r, nil
79+
}
80+
81+
func (o *workspaceRoleType) List(
82+
ctx context.Context,
83+
parentResourceID *v2.ResourceId,
84+
_ resources.SyncOpAttrs,
85+
) (
86+
[]*v2.Resource,
87+
*resources.SyncOpResults,
88+
error,
89+
) {
90+
if parentResourceID == nil {
91+
return nil, &resources.SyncOpResults{}, nil
92+
}
93+
94+
output, err := pkg.MakeResourceList(
95+
ctx,
96+
slices.Collect(maps.Keys(roles)),
97+
parentResourceID,
98+
roleResource,
99+
)
100+
if err != nil {
101+
return nil, nil, err
102+
}
103+
return output, &resources.SyncOpResults{}, nil
104+
}
105+
106+
func (o *workspaceRoleType) Entitlements(
107+
ctx context.Context,
108+
resource *v2.Resource,
109+
attrs resources.SyncOpAttrs,
110+
) (
111+
[]*v2.Entitlement,
112+
*resources.SyncOpResults,
113+
error,
114+
) {
115+
found, missing, err := client.GetWorkspaceNames(ctx, attrs.Session, []string{resource.ParentResourceId.Resource})
116+
if err != nil {
117+
return nil, nil, fmt.Errorf("error getting workspace name for workspace id %s: %w", resource.ParentResourceId.Resource, err)
118+
}
119+
workspaceName, exists := found[resource.ParentResourceId.Resource]
120+
if !exists {
121+
return nil, nil, fmt.Errorf("workspace not found in cache: %s (missing: %v)", resource.ParentResourceId.Resource, missing)
122+
}
123+
return []*v2.Entitlement{
124+
entitlement.NewAssignmentEntitlement(
125+
resource,
126+
RoleAssignmentEntitlement,
127+
entitlement.WithGrantableTo(resourceTypeUser),
128+
entitlement.WithDescription(
129+
fmt.Sprintf(
130+
"Has the %s role in the Slack %s workspace",
131+
resource.DisplayName,
132+
workspaceName,
133+
),
134+
),
135+
entitlement.WithDisplayName(
136+
fmt.Sprintf(
137+
"%s workspace %s role",
138+
workspaceName,
139+
resource.DisplayName,
140+
),
141+
),
142+
),
143+
},
144+
&resources.SyncOpResults{},
145+
nil
146+
}
147+
148+
// Grants would normally return the grants for each role resource. Due to how
149+
// the Slack API works, it is more efficient to emit these roles while listing
150+
// grants for each individual user. Instead of having to list users for each
151+
// role we can divine which roles a user should be granted when calculating
152+
// their grants.
153+
// TLDR: workspaceRoles are set in the workspace.go's Grants method.
154+
func (o *workspaceRoleType) Grants(
155+
_ context.Context,
156+
_ *v2.Resource,
157+
_ resources.SyncOpAttrs,
158+
) (
159+
[]*v2.Grant,
160+
*resources.SyncOpResults,
161+
error,
162+
) {
163+
return nil, &resources.SyncOpResults{}, nil
164+
}

0 commit comments

Comments
 (0)