@@ -2,7 +2,6 @@ package connector
22
33import (
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
1920type 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
2626type 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+
300396func orgRoleBuilder (client * github.Client , orgCache * orgNameCache ) * orgRoleResourceType {
301397 return & orgRoleResourceType {
302398 resourceType : resourceTypeOrgRole ,
0 commit comments