Skip to content

Commit 39384d7

Browse files
authored
Anthony/bitbucket permission fix 2 (#20)
* Added the check to the validate function
1 parent bf29b39 commit 39384d7

File tree

3 files changed

+118
-115
lines changed

3 files changed

+118
-115
lines changed

pkg/bitbucket/client.go

Lines changed: 112 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import (
55
"fmt"
66
"net/http"
77
"net/url"
8+
"strings"
89

910
"github.com/conductorone/baton-sdk/pkg/uhttp"
11+
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
12+
"go.uber.org/zap"
1013
"google.golang.org/grpc/codes"
1114
"google.golang.org/grpc/status"
1215
)
@@ -41,8 +44,9 @@ const (
4144
)
4245

4346
type Client struct {
44-
wrapper *uhttp.BaseHttpClient
45-
scope Scope
47+
wrapper *uhttp.BaseHttpClient
48+
scope Scope
49+
workspaceIDs map[string]bool
4650
}
4751

4852
func NewClient(httpClient *http.Client) *Client {
@@ -108,29 +112,112 @@ func (c *Client) WorkspaceId() (string, error) {
108112
}
109113
}
110114

111-
// If client have access to multiple workspaces, method `WorkspaceIds`
112-
// returns list of workspace ids otherwise it returns error.
113-
func (c *Client) WorkspaceIDs(ctx context.Context) ([]string, error) {
114-
workspaceIDs := make([]string, 0)
115+
func isPermissionDeniedErr(err error) bool {
116+
e, ok := status.FromError(err)
117+
if ok && e.Code() == codes.PermissionDenied {
118+
return true
119+
}
120+
// In most cases the error code is unknown and the error message contains "status 403".
121+
if (!ok || e.Code() == codes.Unknown) && strings.Contains(err.Error(), "status 403") {
122+
return true
123+
}
124+
return false
125+
}
115126

116-
if c.IsUserScoped() {
117-
workspaces, err := c.GetAllWorkspaces(ctx)
118-
if err != nil {
119-
return nil, err
127+
func (c *Client) checkPermissions(ctx context.Context, workspace *Workspace) (bool, error) {
128+
l := ctxzap.Extract(ctx)
129+
logMissingPermission := func(obj string, err error) {
130+
l.Error(
131+
"missing permission to list object in workspace",
132+
zap.String("workspace", workspace.Slug),
133+
zap.String("workspace id", workspace.Id),
134+
zap.String("object", obj),
135+
zap.Error(err),
136+
)
137+
}
138+
paginationVars := PaginationVars{
139+
Limit: 1,
140+
Page: "",
141+
}
142+
_, err := c.GetWorkspaceUserGroups(ctx, workspace.Id)
143+
if err != nil {
144+
if isPermissionDeniedErr(err) {
145+
logMissingPermission("userGroups", err)
146+
return false, nil
120147
}
121-
122-
for _, workspace := range workspaces {
123-
workspaceIDs = append(workspaceIDs, workspace.Id)
148+
return false, err
149+
}
150+
_, _, err = c.GetWorkspaceMembers(ctx, workspace.Id, paginationVars)
151+
if err != nil {
152+
if isPermissionDeniedErr(err) {
153+
logMissingPermission("users", err)
154+
return false, nil
124155
}
156+
return false, err
157+
}
158+
_, _, err = c.GetWorkspaceProjects(ctx, workspace.Id, paginationVars)
159+
if err != nil {
160+
if isPermissionDeniedErr(err) {
161+
logMissingPermission("projects", err)
162+
return false, nil
163+
}
164+
return false, err
165+
}
166+
return true, nil
167+
}
168+
169+
func (c *Client) filterWorkspaces(ctx context.Context, workspaces []Workspace) ([]Workspace, error) {
170+
filteredWorkspaces := make([]Workspace, 0)
125171

126-
if len(workspaceIDs) == 0 {
127-
return nil, status.Error(codes.NotFound, "no workspaces found")
172+
for _, workspace := range workspaces {
173+
// We call this function in order to initialize the workspaceID's map. In that case we need to return all workspaces,
174+
// so they can be filtered and only the valid ones are set in the workspaceIds map.
175+
_, ok := c.workspaceIDs[workspace.Id]
176+
if c.workspaceIDs != nil && !ok {
177+
continue
128178
}
129-
} else {
130-
return nil, status.Error(codes.InvalidArgument, "client is not user scoped")
179+
180+
filteredWorkspaces = append(filteredWorkspaces, workspace)
181+
}
182+
183+
return filteredWorkspaces, nil
184+
}
185+
186+
// If client have access to multiple workspaces, method `WorkspaceIDs`
187+
// returns list of workspace ids otherwise it returns error.
188+
func (c *Client) SetWorkspaceIDs(ctx context.Context, workspaceIDs []string) error {
189+
if !c.IsUserScoped() {
190+
return status.Error(codes.InvalidArgument, "client is not user scoped")
191+
}
192+
c.workspaceIDs = make(map[string]bool)
193+
givenWorkspaceIDs := make(map[string]bool)
194+
for _, workspaceId := range workspaceIDs {
195+
givenWorkspaceIDs[workspaceId] = true
131196
}
132197

133-
return workspaceIDs, nil
198+
workspaces, err := c.GetAllWorkspaces(ctx)
199+
if err != nil {
200+
return err
201+
}
202+
203+
for _, workspace := range workspaces {
204+
workspace := workspace
205+
if _, ok := givenWorkspaceIDs[workspace.Id]; !ok && len(givenWorkspaceIDs) > 0 {
206+
continue
207+
}
208+
ok, err := c.checkPermissions(ctx, &workspace)
209+
if err != nil {
210+
return err
211+
}
212+
if !ok {
213+
continue
214+
}
215+
c.workspaceIDs[workspace.Id] = true
216+
}
217+
if len(c.workspaceIDs) == 0 {
218+
return status.Error(codes.Unauthenticated, "no authenticated workspaces found")
219+
}
220+
return nil
134221
}
135222

136223
// GetWorkspaces lists all workspaces current user belongs to.
@@ -150,7 +237,10 @@ func (c *Client) GetWorkspaces(ctx context.Context, getWorkspacesVars Pagination
150237
prepareFilters(""),
151238
},
152239
)
153-
240+
if err != nil {
241+
return nil, "", err
242+
}
243+
workspacesResponse.Values, err = c.filterWorkspaces(ctx, workspacesResponse.Values)
154244
if err != nil {
155245
return nil, "", err
156246
}
@@ -202,8 +292,10 @@ func (c *Client) GetWorkspace(ctx context.Context, workspaceId string) (*Workspa
202292
prepareFilters(""),
203293
},
204294
)
205-
206295
if err != nil {
296+
if isPermissionDeniedErr(err) {
297+
return nil, status.Error(codes.PermissionDenied, "missing permission to get workspace")
298+
}
207299
return nil, err
208300
}
209301

@@ -228,7 +320,6 @@ func (c *Client) GetWorkspaceMembers(ctx context.Context, workspaceId string, ge
228320
prepareFilters("", "-*.workspace"),
229321
},
230322
)
231-
232323
if err != nil {
233324
return nil, "", err
234325
}

pkg/connector/connector.go

Lines changed: 6 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ import (
88
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
99
"github.com/conductorone/baton-sdk/pkg/annotations"
1010
"github.com/conductorone/baton-sdk/pkg/connectorbuilder"
11-
"github.com/conductorone/baton-sdk/pkg/pagination"
1211
"github.com/conductorone/baton-sdk/pkg/uhttp"
13-
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
14-
"go.uber.org/zap"
1512
)
1613

1714
var (
@@ -68,78 +65,6 @@ func (bb *Bitbucket) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error
6865
}, nil
6966
}
7067

71-
// Get all the valid workspaces, ie. the workspaces that the user has access to.
72-
func (bb *Bitbucket) getValidWorkspaces(ctx context.Context) ([]string, error) {
73-
workspaceObject := workspaceBuilder(bb.client, bb.workspaces)
74-
pageToken := ""
75-
workspaceResources := make([]*v2.Resource, 0)
76-
for {
77-
temp, pageToken, _, err := workspaceObject.List(ctx, nil, &pagination.Token{Token: pageToken})
78-
if err != nil {
79-
return nil, err
80-
}
81-
workspaceResources = append(workspaceResources, temp...)
82-
if pageToken == "" {
83-
break
84-
}
85-
}
86-
87-
validWorkspaces := make([]string, 0)
88-
for _, workspace := range workspaceResources {
89-
ok, err := bb.checkPermissions(ctx, workspace)
90-
if err != nil {
91-
return nil, fmt.Errorf("bitbucket-connector: failed to verify permissions: %w", err)
92-
}
93-
if !ok {
94-
continue
95-
}
96-
validWorkspaces = append(validWorkspaces, workspace.DisplayName)
97-
}
98-
return validWorkspaces, nil
99-
}
100-
101-
func (bb *Bitbucket) checkPermissions(ctx context.Context, workspace *v2.Resource) (bool, error) {
102-
l := ctxzap.Extract(ctx)
103-
logMissingPermission := func(obj string, err error) {
104-
l.Error(
105-
"missing permission to list object in workspace",
106-
zap.String("workspace", workspace.DisplayName),
107-
zap.String("workspace id", workspace.Id.Resource),
108-
zap.String("object", obj),
109-
zap.Error(err),
110-
)
111-
}
112-
paginationVars := bitbucket.PaginationVars{
113-
Limit: 1,
114-
Page: "",
115-
}
116-
_, err := bb.client.GetWorkspaceUserGroups(ctx, workspace.Id.Resource)
117-
if err != nil {
118-
if isPermissionDeniedErr(err) {
119-
logMissingPermission("userGroups", err)
120-
return false, nil
121-
}
122-
return false, err
123-
}
124-
_, _, err = bb.client.GetWorkspaceMembers(ctx, workspace.Id.Resource, paginationVars)
125-
if err != nil {
126-
if isPermissionDeniedErr(err) {
127-
logMissingPermission("users", err)
128-
return false, nil
129-
}
130-
return false, err
131-
}
132-
_, _, err = bb.client.GetWorkspaceProjects(ctx, workspace.Id.Resource, paginationVars)
133-
if err != nil {
134-
if isPermissionDeniedErr(err) {
135-
logMissingPermission("projects", err)
136-
return false, nil
137-
}
138-
return false, err
139-
}
140-
return true, nil
141-
}
142-
14368
// Validate hits the Bitbucket API to validate that the configured credentials are valid and compatible.
14469
func (bb *Bitbucket) Validate(ctx context.Context) (annotations.Annotations, error) {
14570
// get the scope of used credentials
@@ -151,12 +76,12 @@ func (bb *Bitbucket) Validate(ctx context.Context) (annotations.Annotations, err
15176
if err != nil {
15277
return nil, err
15378
}
154-
bb.workspaces, err = bb.getValidWorkspaces(ctx)
155-
if err != nil {
156-
return nil, fmt.Errorf("bitbucket-connector: failed to get valid workspaces: %w", err)
157-
}
158-
if len(bb.workspaces) == 0 {
159-
return nil, fmt.Errorf("bitbucket-connector: no authorized workspaces found")
79+
80+
if bb.client.IsUserScoped() {
81+
err = bb.client.SetWorkspaceIDs(ctx, bb.workspaces)
82+
if err != nil {
83+
return nil, fmt.Errorf("bitbucket-connector: failed to get workspace ids: %w", err)
84+
}
16085
}
16186
return nil, nil
16287
}

pkg/connector/helpers.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import (
99
"github.com/conductorone/baton-sdk/pkg/pagination"
1010
"golang.org/x/text/cases"
1111
"golang.org/x/text/language"
12-
"google.golang.org/grpc/codes"
13-
"google.golang.org/grpc/status"
1412
)
1513

1614
var ResourcesPageSize = 50
@@ -93,14 +91,3 @@ func ParseEntitlementID(id string) (*v2.ResourceId, string, error) {
9391
}
9492
return resourceId, parts[len(parts)-1], nil
9593
}
96-
func isPermissionDeniedErr(err error) bool {
97-
e, ok := status.FromError(err)
98-
if ok && e.Code() == codes.PermissionDenied {
99-
return true
100-
}
101-
// In most cases the error code is unknown and the error message contains "status 403".
102-
if (!ok || e.Code() == codes.Unknown) && strings.Contains(err.Error(), "status 403") {
103-
return true
104-
}
105-
return false
106-
}

0 commit comments

Comments
 (0)