Skip to content

Commit 1f108ab

Browse files
committed
clean up and added grant/revoke
1 parent 0c95056 commit 1f108ab

File tree

1 file changed

+203
-107
lines changed

1 file changed

+203
-107
lines changed

pkg/connector/org_role.go

Lines changed: 203 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package connector
22

33
import (
44
"context"
5-
"encoding/json"
65
"fmt"
76
"net/http"
87
"strconv"
@@ -14,13 +13,14 @@ import (
1413
"github.com/conductorone/baton-sdk/pkg/types/grant"
1514
"github.com/conductorone/baton-sdk/pkg/types/resource"
1615
"github.com/google/go-github/v63/github"
16+
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
17+
"go.uber.org/zap"
1718
)
1819

1920
type OrganizationRole struct {
20-
ID int64 `json:"id"`
21-
Name string `json:"name"`
22-
Description string `json:"description"`
23-
Permissions []string `json:"permissions"`
21+
ID int64 `json:"id"`
22+
Name string `json:"name"`
23+
Description string `json:"description"`
2424
}
2525

2626
type OrganizationRoleResponse struct {
@@ -85,51 +85,33 @@ func (o *orgRoleResourceType) List(
8585
return nil, "", nil, err
8686
}
8787

88-
// Use REST API directly since the client doesn't support these endpoints yet
89-
url := fmt.Sprintf("https://api.github.com/orgs/%s/organization-roles", orgName)
90-
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
88+
roles, resp, err := o.client.Organizations.ListRoles(ctx, orgName)
9189
if err != nil {
92-
return nil, "", nil, fmt.Errorf("failed to create request: %w", err)
93-
}
94-
95-
req.Header.Set("Accept", "application/vnd.github+json")
96-
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
97-
98-
resp, err := o.client.Client().Do(req)
99-
if err != nil {
100-
return nil, "", nil, fmt.Errorf("failed to list organization roles: %w", err)
101-
}
102-
defer resp.Body.Close()
103-
104-
// Handle permission errors gracefully
105-
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound {
106-
// Return empty list with no error to indicate we skipped this resource
107-
pageToken, err := bag.NextToken("")
108-
if err != nil {
109-
return nil, "", nil, err
90+
// Handle permission errors gracefully
91+
if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound) {
92+
// Return empty list with no error to indicate we skipped this resource
93+
pageToken, err := bag.NextToken("")
94+
if err != nil {
95+
return nil, "", nil, err
96+
}
97+
return nil, pageToken, nil, nil
11098
}
111-
return nil, pageToken, nil, nil
112-
}
113-
114-
if resp.StatusCode != http.StatusOK {
115-
return nil, "", nil, fmt.Errorf("failed to list organization roles: %s", resp.Status)
116-
}
117-
118-
var roleResp OrganizationRoleResponse
119-
if err := json.NewDecoder(resp.Body).Decode(&roleResp); err != nil {
120-
return nil, "", nil, fmt.Errorf("failed to decode response: %w", err)
99+
return nil, "", nil, fmt.Errorf("failed to list organization roles: %w", err)
121100
}
122101

123102
var ret []*v2.Resource
124-
for _, role := range roleResp.Roles {
125-
roleResource, err := orgRoleResource(ctx, &role, &v2.Resource{Id: parentID})
103+
for _, role := range roles.CustomRepoRoles {
104+
roleResource, err := orgRoleResource(ctx, &OrganizationRole{
105+
ID: role.GetID(),
106+
Name: role.GetName(),
107+
Description: role.GetDescription(),
108+
}, &v2.Resource{Id: parentID})
126109
if err != nil {
127110
return nil, "", nil, err
128111
}
129112
ret = append(ret, roleResource)
130113
}
131114

132-
// Since the API doesn't support pagination for roles yet, we'll return an empty token
133115
pageToken, err := bag.NextToken("")
134116
if err != nil {
135117
return nil, "", nil, err
@@ -183,43 +165,23 @@ func (o *orgRoleResourceType) Grants(
183165
var ret []*v2.Grant
184166

185167
// First, get teams with this role
186-
url := fmt.Sprintf("https://api.github.com/orgs/%s/organization-roles/%d/teams", orgName, roleID)
187-
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
168+
teams, resp, err := o.client.Organizations.ListTeamsAssignedToOrgRole(ctx, orgName, roleID, nil)
188169
if err != nil {
189-
return nil, "", nil, fmt.Errorf("failed to create request: %w", err)
190-
}
191-
192-
req.Header.Set("Accept", "application/vnd.github+json")
193-
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
194-
195-
resp, err := o.client.Client().Do(req)
196-
if err != nil {
197-
return nil, "", nil, fmt.Errorf("failed to list role teams: %w", err)
198-
}
199-
defer resp.Body.Close()
200-
201-
// Handle permission errors gracefully
202-
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound {
203-
// Return empty list with no error to indicate we skipped this resource
204-
pageToken, err := bag.NextToken("")
205-
if err != nil {
206-
return nil, "", nil, err
170+
// Handle permission errors gracefully
171+
if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound) {
172+
// Return empty list with no error to indicate we skipped this resource
173+
pageToken, err := bag.NextToken("")
174+
if err != nil {
175+
return nil, "", nil, err
176+
}
177+
return nil, pageToken, nil, nil
207178
}
208-
return nil, pageToken, nil, nil
209-
}
210-
211-
if resp.StatusCode != http.StatusOK {
212-
return nil, "", nil, fmt.Errorf("failed to list role teams: %s", resp.Status)
213-
}
214-
215-
var teams []OrganizationRoleTeam
216-
if err := json.NewDecoder(resp.Body).Decode(&teams); err != nil {
217-
return nil, "", nil, fmt.Errorf("failed to decode response: %w", err)
179+
return nil, "", nil, fmt.Errorf("failed to list role teams: %w", err)
218180
}
219181

220182
// Create expandable grants for teams
221183
for _, team := range teams {
222-
teamResource, err := teamResource(&github.Team{ID: &team.ID, Name: &team.Name}, resource.ParentResourceId)
184+
teamResource, err := teamResource(team, resource.ParentResourceId)
223185
if err != nil {
224186
return nil, "", nil, err
225187
}
@@ -230,65 +192,43 @@ func (o *orgRoleResourceType) Grants(
230192
"assigned",
231193
teamResource.Id,
232194
grant.WithAnnotation(&v2.GrantExpandable{
233-
EntitlementIds: []string{fmt.Sprintf("team:%d:member", team.ID)},
195+
EntitlementIds: []string{fmt.Sprintf("team:%d:member", team.GetID())},
234196
Shallow: true,
235197
}),
236198
)
237199
ret = append(ret, grant)
238200
}
239201

240202
// Then, get direct user assignments
241-
url = fmt.Sprintf("https://api.github.com/orgs/%s/organization-roles/%d/users", orgName, roleID)
242-
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
243-
if err != nil {
244-
return nil, "", nil, fmt.Errorf("failed to create request: %w", err)
245-
}
246-
247-
req.Header.Set("Accept", "application/vnd.github+json")
248-
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
249-
250-
resp, err = o.client.Client().Do(req)
203+
users, resp, err := o.client.Organizations.ListUsersAssignedToOrgRole(ctx, orgName, roleID, nil)
251204
if err != nil {
205+
// Handle permission errors gracefully
206+
if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound) {
207+
// Return what we have so far (teams) with no error
208+
pageToken, err := bag.NextToken("")
209+
if err != nil {
210+
return nil, "", nil, err
211+
}
212+
return ret, pageToken, nil, nil
213+
}
252214
return nil, "", nil, fmt.Errorf("failed to list role users: %w", err)
253215
}
254-
defer resp.Body.Close()
255216

256-
// Handle permission errors gracefully
257-
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound {
258-
// Return what we have so far (teams) with no error
259-
pageToken, err := bag.NextToken("")
217+
// Create regular grants for direct user assignments
218+
for _, user := range users {
219+
userResource, err := userResource(ctx, user, user.GetEmail(), nil)
260220
if err != nil {
261221
return nil, "", nil, err
262222
}
263-
return ret, pageToken, nil, nil
264-
}
265-
266-
if resp.StatusCode != http.StatusOK {
267-
return nil, "", nil, fmt.Errorf("failed to list role users: %s", resp.Status)
268-
}
269223

270-
var users []*github.User
271-
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
272-
return nil, "", nil, fmt.Errorf("failed to decode response: %w", err)
273-
}
274-
275-
// Create regular grants for direct user assignments
276-
for _, user := range users {
277224
grant := grant.NewGrant(
278225
resource,
279226
"assigned",
280-
&v2.ResourceId{
281-
ResourceType: resourceTypeUser.Id,
282-
Resource: fmt.Sprintf("%d", user.GetID()),
283-
},
284-
grant.WithAnnotation(&v2.V1Identifier{
285-
Id: fmt.Sprintf("org_role_grant:%s:%d", resource.Id.Resource, user.GetID()),
286-
}),
227+
userResource.Id,
287228
)
288229
ret = append(ret, grant)
289230
}
290231

291-
// Since the API doesn't support pagination for role teams/users yet, we'll return an empty token
292232
pageToken, err := bag.NextToken("")
293233
if err != nil {
294234
return nil, "", nil, err
@@ -297,6 +237,162 @@ func (o *orgRoleResourceType) Grants(
297237
return ret, pageToken, nil, nil
298238
}
299239

240+
func (o *orgRoleResourceType) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error) {
241+
l := ctxzap.Extract(ctx)
242+
243+
if principal.Id.ResourceType != resourceTypeUser.Id {
244+
l.Warn(
245+
"github-connectorv2: only users can be granted organization roles",
246+
zap.String("principal_type", principal.Id.ResourceType),
247+
zap.String("principal_id", principal.Id.Resource),
248+
)
249+
return nil, fmt.Errorf("github-connectorv2: only users can be granted organization roles")
250+
}
251+
252+
roleID, err := strconv.ParseInt(entitlement.Resource.Id.Resource, 10, 64)
253+
if err != nil {
254+
return nil, fmt.Errorf("invalid role ID: %w", err)
255+
}
256+
257+
orgName, err := o.orgCache.GetOrgName(ctx, entitlement.Resource.ParentResourceId)
258+
if err != nil {
259+
return nil, fmt.Errorf("failed to get org name: %w", err)
260+
}
261+
262+
// First verify that the role exists
263+
roles, resp, err := o.client.Organizations.ListRoles(ctx, orgName)
264+
if err != nil {
265+
if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound) {
266+
return nil, fmt.Errorf("failed to verify role: organization not found or insufficient permissions")
267+
}
268+
return nil, fmt.Errorf("failed to verify role: %w", err)
269+
}
270+
271+
// Check if the role exists
272+
roleExists := false
273+
for _, role := range roles.CustomRepoRoles {
274+
if role.GetID() == roleID {
275+
roleExists = true
276+
break
277+
}
278+
}
279+
280+
if !roleExists {
281+
return nil, fmt.Errorf("role with ID %d not found in organization %s", roleID, orgName)
282+
}
283+
284+
userID, err := strconv.ParseInt(principal.Id.Resource, 10, 64)
285+
if err != nil {
286+
return nil, fmt.Errorf("invalid user ID: %w", err)
287+
}
288+
289+
user, _, err := o.client.Users.GetByID(ctx, userID)
290+
if err != nil {
291+
return nil, fmt.Errorf("failed to get user: %w", err)
292+
}
293+
294+
l.Info("attempting to assign role",
295+
zap.String("org", orgName),
296+
zap.Int64("role_id", roleID),
297+
zap.String("user", user.GetLogin()),
298+
)
299+
300+
// Use the client's HTTP client to make the request with the correct URL format
301+
url := fmt.Sprintf("orgs/%s/organization-roles/users/%s/%d", orgName, user.GetLogin(), roleID)
302+
req, err := o.client.NewRequest("PUT", url, nil)
303+
if err != nil {
304+
return nil, fmt.Errorf("failed to create request: %w", err)
305+
}
306+
307+
resp, err = o.client.Do(ctx, req, nil)
308+
if err != nil {
309+
if resp != nil {
310+
l.Error("failed to assign role",
311+
zap.String("org", orgName),
312+
zap.Int64("role_id", roleID),
313+
zap.String("user", user.GetLogin()),
314+
zap.Int("status_code", resp.StatusCode),
315+
zap.String("status", resp.Status),
316+
zap.Error(err),
317+
)
318+
}
319+
return nil, fmt.Errorf("failed to assign role: %w", err)
320+
}
321+
322+
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
323+
l.Error("failed to assign role",
324+
zap.String("org", orgName),
325+
zap.Int64("role_id", roleID),
326+
zap.String("user", user.GetLogin()),
327+
zap.Int("status_code", resp.StatusCode),
328+
zap.String("status", resp.Status),
329+
)
330+
return nil, fmt.Errorf("failed to assign role: %s", resp.Status)
331+
}
332+
333+
l.Info("successfully assigned role",
334+
zap.String("org", orgName),
335+
zap.Int64("role_id", roleID),
336+
zap.String("user", user.GetLogin()),
337+
)
338+
339+
return nil, nil
340+
}
341+
342+
func (o *orgRoleResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) {
343+
l := ctxzap.Extract(ctx)
344+
345+
entitlement := grant.Entitlement
346+
principal := grant.Principal
347+
348+
if principal.Id.ResourceType != resourceTypeUser.Id {
349+
l.Warn(
350+
"github-connectorv2: only users can have organization roles revoked",
351+
zap.String("principal_type", principal.Id.ResourceType),
352+
zap.String("principal_id", principal.Id.Resource),
353+
)
354+
return nil, fmt.Errorf("github-connectorv2: only users can have organization roles revoked")
355+
}
356+
357+
roleID, err := strconv.ParseInt(entitlement.Resource.Id.Resource, 10, 64)
358+
if err != nil {
359+
return nil, fmt.Errorf("invalid role ID: %w", err)
360+
}
361+
362+
orgName, err := o.orgCache.GetOrgName(ctx, entitlement.Resource.ParentResourceId)
363+
if err != nil {
364+
return nil, fmt.Errorf("failed to get org name: %w", err)
365+
}
366+
367+
userID, err := strconv.ParseInt(principal.Id.Resource, 10, 64)
368+
if err != nil {
369+
return nil, fmt.Errorf("invalid user ID: %w", err)
370+
}
371+
372+
user, _, err := o.client.Users.GetByID(ctx, userID)
373+
if err != nil {
374+
return nil, fmt.Errorf("failed to get user: %w", err)
375+
}
376+
377+
// Use the client's HTTP client to make the request with the correct URL format
378+
url := fmt.Sprintf("orgs/%s/organization-roles/users/%s/%d", orgName, user.GetLogin(), roleID)
379+
req, err := o.client.NewRequest("DELETE", url, nil)
380+
if err != nil {
381+
return nil, fmt.Errorf("failed to create request: %w", err)
382+
}
383+
384+
resp, err := o.client.Do(ctx, req, nil)
385+
if err != nil {
386+
return nil, fmt.Errorf("failed to revoke role: %w", err)
387+
}
388+
389+
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
390+
return nil, fmt.Errorf("failed to revoke role: %s", resp.Status)
391+
}
392+
393+
return nil, nil
394+
}
395+
300396
func orgRoleBuilder(client *github.Client, orgCache *orgNameCache) *orgRoleResourceType {
301397
return &orgRoleResourceType{
302398
resourceType: resourceTypeOrgRole,

0 commit comments

Comments
 (0)