Skip to content

Commit 0c95056

Browse files
committed
initial support for org roles.
1 parent ba90aec commit 0c95056

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed

pkg/connector/connector.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ var (
4949
},
5050
Annotations: v1AnnotationsForResourceType("user"),
5151
}
52+
resourceTypeOrgRole = &v2.ResourceType{
53+
Id: "org_role",
54+
DisplayName: "Organization Role",
55+
Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_ROLE},
56+
Annotations: v1AnnotationsForResourceType("org_role"),
57+
}
5258
)
5359

5460
type GitHub struct {
@@ -66,6 +72,7 @@ func (gh *GitHub) ResourceSyncers(ctx context.Context) []connectorbuilder.Resour
6672
teamBuilder(gh.client, gh.orgCache),
6773
userBuilder(gh.client, gh.hasSAMLEnabled, gh.graphqlClient, gh.orgCache),
6874
repositoryBuilder(gh.client, gh.orgCache),
75+
orgRoleBuilder(gh.client, gh.orgCache),
6976
}
7077
}
7178

pkg/connector/org.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func organizationResource(
5252
&v2.ChildResourceType{ResourceTypeId: resourceTypeUser.Id},
5353
&v2.ChildResourceType{ResourceTypeId: resourceTypeTeam.Id},
5454
&v2.ChildResourceType{ResourceTypeId: resourceTypeRepository.Id},
55+
&v2.ChildResourceType{ResourceTypeId: resourceTypeOrgRole.Id},
5556
),
5657
)
5758
}

pkg/connector/org_role.go

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
package connector
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"strconv"
9+
10+
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
11+
"github.com/conductorone/baton-sdk/pkg/annotations"
12+
"github.com/conductorone/baton-sdk/pkg/pagination"
13+
"github.com/conductorone/baton-sdk/pkg/types/entitlement"
14+
"github.com/conductorone/baton-sdk/pkg/types/grant"
15+
"github.com/conductorone/baton-sdk/pkg/types/resource"
16+
"github.com/google/go-github/v63/github"
17+
)
18+
19+
type OrganizationRole struct {
20+
ID int64 `json:"id"`
21+
Name string `json:"name"`
22+
Description string `json:"description"`
23+
Permissions []string `json:"permissions"`
24+
}
25+
26+
type OrganizationRoleResponse struct {
27+
TotalCount int `json:"total_count"`
28+
Roles []OrganizationRole `json:"roles"`
29+
}
30+
31+
type OrganizationRoleTeam struct {
32+
ID int64 `json:"id"`
33+
Name string `json:"name"`
34+
}
35+
36+
type orgRoleResourceType struct {
37+
resourceType *v2.ResourceType
38+
client *github.Client
39+
orgCache *orgNameCache
40+
}
41+
42+
func orgRoleResource(
43+
ctx context.Context,
44+
role *OrganizationRole,
45+
org *v2.Resource,
46+
) (*v2.Resource, error) {
47+
profile := map[string]interface{}{
48+
"description": role.Description,
49+
}
50+
51+
return resource.NewRoleResource(
52+
role.Name,
53+
resourceTypeOrgRole,
54+
role.ID,
55+
[]resource.RoleTraitOption{
56+
resource.WithRoleProfile(profile),
57+
},
58+
resource.WithParentResourceID(org.Id),
59+
resource.WithAnnotation(
60+
&v2.V1Identifier{Id: fmt.Sprintf("org_role:%d", role.ID)},
61+
),
62+
)
63+
}
64+
65+
func (o *orgRoleResourceType) ResourceType(_ context.Context) *v2.ResourceType {
66+
return o.resourceType
67+
}
68+
69+
func (o *orgRoleResourceType) List(
70+
ctx context.Context,
71+
parentID *v2.ResourceId,
72+
pToken *pagination.Token,
73+
) ([]*v2.Resource, string, annotations.Annotations, error) {
74+
if parentID == nil {
75+
return nil, "", nil, nil
76+
}
77+
78+
bag, _, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: resourceTypeOrgRole.Id})
79+
if err != nil {
80+
return nil, "", nil, err
81+
}
82+
83+
orgName, err := o.orgCache.GetOrgName(ctx, parentID)
84+
if err != nil {
85+
return nil, "", nil, err
86+
}
87+
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)
91+
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
110+
}
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)
121+
}
122+
123+
var ret []*v2.Resource
124+
for _, role := range roleResp.Roles {
125+
roleResource, err := orgRoleResource(ctx, &role, &v2.Resource{Id: parentID})
126+
if err != nil {
127+
return nil, "", nil, err
128+
}
129+
ret = append(ret, roleResource)
130+
}
131+
132+
// Since the API doesn't support pagination for roles yet, we'll return an empty token
133+
pageToken, err := bag.NextToken("")
134+
if err != nil {
135+
return nil, "", nil, err
136+
}
137+
138+
return ret, pageToken, nil, nil
139+
}
140+
141+
func (o *orgRoleResourceType) Entitlements(
142+
_ context.Context,
143+
resource *v2.Resource,
144+
_ *pagination.Token,
145+
) ([]*v2.Entitlement, string, annotations.Annotations, error) {
146+
rv := make([]*v2.Entitlement, 0, 1)
147+
rv = append(rv, entitlement.NewAssignmentEntitlement(resource, "assigned",
148+
entitlement.WithDisplayName(resource.DisplayName),
149+
entitlement.WithDescription(fmt.Sprintf("Assignment to %s role in GitHub", resource.DisplayName)),
150+
entitlement.WithAnnotation(&v2.V1Identifier{
151+
Id: fmt.Sprintf("org_role:%s", resource.Id.Resource),
152+
}),
153+
entitlement.WithGrantableTo(resourceTypeUser),
154+
))
155+
156+
return rv, "", nil, nil
157+
}
158+
159+
func (o *orgRoleResourceType) Grants(
160+
ctx context.Context,
161+
resource *v2.Resource,
162+
pToken *pagination.Token,
163+
) ([]*v2.Grant, string, annotations.Annotations, error) {
164+
if resource == nil {
165+
return nil, "", nil, nil
166+
}
167+
168+
bag, _, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: resourceTypeOrgRole.Id})
169+
if err != nil {
170+
return nil, "", nil, err
171+
}
172+
173+
orgName, err := o.orgCache.GetOrgName(ctx, resource.ParentResourceId)
174+
if err != nil {
175+
return nil, "", nil, err
176+
}
177+
178+
roleID, err := strconv.ParseInt(resource.Id.Resource, 10, 64)
179+
if err != nil {
180+
return nil, "", nil, fmt.Errorf("invalid role ID: %w", err)
181+
}
182+
183+
var ret []*v2.Grant
184+
185+
// 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)
188+
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
207+
}
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)
218+
}
219+
220+
// Create expandable grants for teams
221+
for _, team := range teams {
222+
teamResource, err := teamResource(&github.Team{ID: &team.ID, Name: &team.Name}, resource.ParentResourceId)
223+
if err != nil {
224+
return nil, "", nil, err
225+
}
226+
227+
// Create an expandable grant for the team
228+
grant := grant.NewGrant(
229+
resource,
230+
"assigned",
231+
teamResource.Id,
232+
grant.WithAnnotation(&v2.GrantExpandable{
233+
EntitlementIds: []string{fmt.Sprintf("team:%d:member", team.ID)},
234+
Shallow: true,
235+
}),
236+
)
237+
ret = append(ret, grant)
238+
}
239+
240+
// 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)
251+
if err != nil {
252+
return nil, "", nil, fmt.Errorf("failed to list role users: %w", err)
253+
}
254+
defer resp.Body.Close()
255+
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("")
260+
if err != nil {
261+
return nil, "", nil, err
262+
}
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+
}
269+
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 {
277+
grant := grant.NewGrant(
278+
resource,
279+
"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+
}),
287+
)
288+
ret = append(ret, grant)
289+
}
290+
291+
// Since the API doesn't support pagination for role teams/users yet, we'll return an empty token
292+
pageToken, err := bag.NextToken("")
293+
if err != nil {
294+
return nil, "", nil, err
295+
}
296+
297+
return ret, pageToken, nil, nil
298+
}
299+
300+
func orgRoleBuilder(client *github.Client, orgCache *orgNameCache) *orgRoleResourceType {
301+
return &orgRoleResourceType{
302+
resourceType: resourceTypeOrgRole,
303+
client: client,
304+
orgCache: orgCache,
305+
}
306+
}

0 commit comments

Comments
 (0)