Skip to content

Commit e66409a

Browse files
mstanbCOmichael burton
andauthored
adds grabbing users saml email addresses if they exist, and fixes a bug with listing teams (#13)
Co-authored-by: michael burton <[email protected]>
1 parent ad86e7a commit e66409a

File tree

26 files changed

+7719
-21
lines changed

26 files changed

+7719
-21
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/conductorone/baton-sdk v0.0.17
77
github.com/google/go-github/v41 v41.0.0
88
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
9+
github.com/shurcooL/githubv4 v0.0.0-20221229060216-a8d4a561cc93
910
github.com/spf13/cobra v1.6.1
1011
go.uber.org/zap v1.24.0
1112
golang.org/x/oauth2 v0.2.0
@@ -53,6 +54,7 @@ require (
5354
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
5455
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
5556
github.com/segmentio/ksuid v1.0.4 // indirect
57+
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect
5658
github.com/spf13/afero v1.9.3 // indirect
5759
github.com/spf13/cast v1.5.0 // indirect
5860
github.com/spf13/jwalterweatherman v1.1.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO
256256
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
257257
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
258258
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
259+
github.com/shurcooL/githubv4 v0.0.0-20221229060216-a8d4a561cc93 h1:JNy04upyaTaAGVlUFAL+60/1nphmJtuTu36tLhbaqXk=
260+
github.com/shurcooL/githubv4 v0.0.0-20221229060216-a8d4a561cc93/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
261+
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc=
262+
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
259263
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
260264
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
261265
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=

pkg/connector/connector.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/conductorone/baton-sdk/pkg/uhttp"
1313
"github.com/google/go-github/v41/github"
1414
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
15+
"github.com/shurcooL/githubv4"
1516
"golang.org/x/oauth2"
1617
"google.golang.org/grpc/codes"
1718
"google.golang.org/grpc/status"
@@ -49,16 +50,18 @@ var (
4950
)
5051

5152
type Github struct {
52-
orgs []string
53-
client *github.Client
54-
instanceURL string
53+
orgs []string
54+
client *github.Client
55+
instanceURL string
56+
graphqlClient *githubv4.Client
57+
hasSAMLEnabled *bool
5558
}
5659

5760
func (gh *Github) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncer {
5861
return []connectorbuilder.ResourceSyncer{
5962
orgBuilder(gh.client, gh.orgs),
6063
teamBuilder(gh.client),
61-
userBuilder(gh.client),
64+
userBuilder(gh.client, gh.hasSAMLEnabled, gh.graphqlClient),
6265
repositoryBuilder(gh.client),
6366
}
6467
}
@@ -155,11 +158,31 @@ func New(ctx context.Context, githubOrgs []string, instanceURL, accessToken stri
155158
if err != nil {
156159
return nil, err
157160
}
161+
graphqlClient, err := newGithubGraphqlClient(ctx, accessToken)
162+
if err != nil {
163+
return nil, err
164+
}
158165
gh := &Github{
159-
client: client,
160-
instanceURL: instanceURL,
161-
orgs: githubOrgs,
166+
client: client,
167+
instanceURL: instanceURL,
168+
orgs: githubOrgs,
169+
graphqlClient: graphqlClient,
162170
}
163171

164172
return gh, nil
165173
}
174+
175+
func newGithubGraphqlClient(ctx context.Context, accessToken string) (*githubv4.Client, error) {
176+
httpClient, err := uhttp.NewClient(ctx, uhttp.WithLogger(true, ctxzap.Extract(ctx)))
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
182+
183+
ts := oauth2.StaticTokenSource(
184+
&oauth2.Token{AccessToken: accessToken},
185+
)
186+
tc := oauth2.NewClient(ctx, ts)
187+
return githubv4.NewClient(tc), nil
188+
}

pkg/connector/helpers.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/conductorone/baton-sdk/pkg/annotations"
1111
"github.com/conductorone/baton-sdk/pkg/pagination"
1212
"github.com/google/go-github/v41/github"
13+
"github.com/shurcooL/githubv4"
1314
"golang.org/x/text/cases"
1415
"golang.org/x/text/language"
1516
"google.golang.org/protobuf/types/known/timestamppb"
@@ -140,3 +141,37 @@ func extractRateLimitData(response *github.Response) (*v2.RateLimitDescription,
140141
ResetAt: ra,
141142
}, nil
142143
}
144+
145+
type listUsersQuery struct {
146+
Organization struct {
147+
SamlIdentityProvider struct {
148+
SsoUrl githubv4.String
149+
ExternalIdentities struct {
150+
Edges []struct {
151+
Node struct {
152+
SamlIdentity struct {
153+
NameId string
154+
}
155+
User struct {
156+
Login string
157+
}
158+
}
159+
}
160+
} `graphql:"externalIdentities(first: 1, login: $userName)"`
161+
}
162+
} `graphql:"organization(login: $orgLoginName)"`
163+
RateLimit struct {
164+
Limit int
165+
Cost int
166+
Remaining int
167+
ResetAt githubv4.DateTime
168+
}
169+
}
170+
171+
type hasSAMLQuery struct {
172+
Organization struct {
173+
SamlIdentityProvider struct {
174+
Id string
175+
}
176+
} `graphql:"organization(login: $orgLoginName)"`
177+
}

pkg/connector/org.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func (o *orgResourceType) Grants(
170170
continue
171171
}
172172

173-
ur, err := userResource(ctx, user)
173+
ur, err := userResource(ctx, user, user.GetEmail())
174174
if err != nil {
175175
return nil, "", nil, err
176176
}

pkg/connector/repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func (o *repositoryResourceType) Grants(
183183
Id: fmt.Sprintf("repo-grant:%s:%d:%s", resource.Id.Resource, user.GetID(), permission),
184184
})
185185

186-
ur, err := userResource(ctx, user)
186+
ur, err := userResource(ctx, user, user.GetEmail())
187187
if err != nil {
188188
return nil, "", nil, err
189189
}

pkg/connector/team.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (o *teamResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt
8383
switch bag.ResourceID() {
8484
// No resource ID set, so just list teams and push an action for each that we see
8585
case "":
86-
bag.Pop()
86+
pageState := bag.Pop()
8787
orgName, err := getOrgName(ctx, o.client, parentID)
8888
if err != nil {
8989
return nil, "", nil, err
@@ -94,6 +94,9 @@ func (o *teamResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt
9494
return nil, "", nil, fmt.Errorf("github-connector: failed to list teams: %w", err)
9595
}
9696

97+
if len(teams) == 0 {
98+
bag.Push(*pageState)
99+
}
97100
for _, t := range teams {
98101
bag.Push(pagination.PageState{
99102
ResourceTypeID: resourceTypeTeam.Id,
@@ -230,7 +233,7 @@ func (o *teamResourceType) Grants(ctx context.Context, resource *v2.Resource, pT
230233
Id: fmt.Sprintf("team-grant:%s:%d:%s", resource.Id.Resource, user.GetID(), membership.GetRole()),
231234
})
232235

233-
ur, err := userResource(ctx, user)
236+
ur, err := userResource(ctx, user, user.GetEmail())
234237
if err != nil {
235238
return nil, "", nil, err
236239
}

pkg/connector/user.go

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import (
1111
"github.com/conductorone/baton-sdk/pkg/pagination"
1212
"github.com/conductorone/baton-sdk/pkg/sdk"
1313
"github.com/google/go-github/v41/github"
14+
"github.com/shurcooL/githubv4"
15+
"google.golang.org/protobuf/types/known/timestamppb"
1416
)
1517

1618
// Create a new connector resource for a github user.
17-
func userResource(ctx context.Context, user *github.User) (*v2.Resource, error) {
19+
func userResource(ctx context.Context, user *github.User, userEmail string) (*v2.Resource, error) {
1820
displayName := user.GetName()
1921
if displayName == "" {
2022
// users do not always specify a name and we only get public email from
@@ -44,7 +46,7 @@ func userResource(ctx context.Context, user *github.User) (*v2.Resource, error)
4446
resourceTypeUser,
4547
nil,
4648
user.GetID(),
47-
user.GetEmail(),
49+
userEmail,
4850
profile,
4951
&v2.ExternalLink{Url: user.GetHTMLURL()},
5052
&v2.V1Identifier{Id: strconv.FormatInt(user.GetID(), 10)},
@@ -57,15 +59,18 @@ func userResource(ctx context.Context, user *github.User) (*v2.Resource, error)
5759
}
5860

5961
type userResourceType struct {
60-
resourceType *v2.ResourceType
61-
client *github.Client
62+
resourceType *v2.ResourceType
63+
client *github.Client
64+
graphqlClient *githubv4.Client
65+
hasSAMLEnabled *bool
6266
}
6367

6468
func (o *userResourceType) ResourceType(_ context.Context) *v2.ResourceType {
6569
return o.resourceType
6670
}
6771

6872
func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) {
73+
var annotations annotations.Annotations
6974
if parentID == nil {
7075
return nil, "", nil, nil
7176
}
@@ -80,6 +85,13 @@ func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt
8085
return nil, "", nil, err
8186
}
8287

88+
hasSamlBool, err := o.hasSAML(ctx, orgName)
89+
if err != nil {
90+
return nil, "", nil, err
91+
}
92+
q := listUsersQuery{}
93+
var restApiRateLimit *v2.RateLimitDescription
94+
8395
opts := github.ListMembersOptions{
8496
ListOptions: github.ListOptions{Page: page, PerPage: pt.Size},
8597
}
@@ -89,7 +101,12 @@ func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt
89101
return nil, "", nil, fmt.Errorf("github-connector: ListMembers failed: %w", err)
90102
}
91103

92-
nextPage, reqAnnos, err := parseResp(resp)
104+
restApiRateLimit, err = extractRateLimitData(resp)
105+
if err != nil {
106+
return nil, "", nil, err
107+
}
108+
109+
nextPage, _, err := parseResp(resp)
93110
if err != nil {
94111
return nil, "", nil, err
95112
}
@@ -105,15 +122,39 @@ func (o *userResourceType) List(ctx context.Context, parentID *v2.ResourceId, pt
105122
if err != nil {
106123
return nil, "", nil, err
107124
}
108-
ur, err := userResource(ctx, u)
125+
userEmail := u.GetEmail()
126+
if hasSamlBool {
127+
q = listUsersQuery{}
128+
variables := map[string]interface{}{
129+
"orgLoginName": githubv4.String(orgName),
130+
"userName": githubv4.String(u.GetLogin()),
131+
}
132+
err = o.graphqlClient.Query(ctx, &q, variables)
133+
if err != nil {
134+
return nil, "", nil, err
135+
}
136+
if len(q.Organization.SamlIdentityProvider.ExternalIdentities.Edges) == 1 {
137+
userEmail = q.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node.SamlIdentity.NameId
138+
}
139+
}
140+
ur, err := userResource(ctx, u, userEmail)
109141
if err != nil {
110142
return nil, "", nil, err
111143
}
112144

113145
rv = append(rv, ur)
114146
}
147+
annotations.WithRateLimiting(restApiRateLimit)
148+
if *o.hasSAMLEnabled && int64(q.RateLimit.Remaining) < restApiRateLimit.Remaining {
149+
graphqlRateLimit := &v2.RateLimitDescription{
150+
Limit: int64(q.RateLimit.Limit),
151+
Remaining: int64(q.RateLimit.Remaining),
152+
ResetAt: timestamppb.New(q.RateLimit.ResetAt.Time),
153+
}
154+
annotations.WithRateLimiting(graphqlRateLimit)
155+
}
115156

116-
return rv, pageToken, reqAnnos, nil
157+
return rv, pageToken, annotations, nil
117158
}
118159

119160
func (o *userResourceType) Entitlements(_ context.Context, _ *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) {
@@ -124,9 +165,32 @@ func (o *userResourceType) Grants(_ context.Context, _ *v2.Resource, _ *paginati
124165
return nil, "", nil, nil
125166
}
126167

127-
func userBuilder(client *github.Client) *userResourceType {
168+
func userBuilder(client *github.Client, hasSAMLEnabled *bool, graphqlClient *githubv4.Client) *userResourceType {
128169
return &userResourceType{
129-
resourceType: resourceTypeUser,
130-
client: client,
170+
resourceType: resourceTypeUser,
171+
client: client,
172+
graphqlClient: graphqlClient,
173+
hasSAMLEnabled: hasSAMLEnabled,
174+
}
175+
}
176+
177+
func (o *userResourceType) hasSAML(ctx context.Context, orgName string) (bool, error) {
178+
if o.hasSAMLEnabled != nil {
179+
return *o.hasSAMLEnabled, nil
180+
}
181+
182+
samlBool := false
183+
q := hasSAMLQuery{}
184+
variables := map[string]interface{}{
185+
"orgLoginName": githubv4.String(orgName),
186+
}
187+
err := o.graphqlClient.Query(ctx, &q, variables)
188+
if err != nil {
189+
return false, err
190+
}
191+
if q.Organization.SamlIdentityProvider.Id != "" {
192+
samlBool = true
131193
}
194+
o.hasSAMLEnabled = &samlBool
195+
return *o.hasSAMLEnabled, nil
132196
}

vendor/github.com/shurcooL/githubv4/.travis.yml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/shurcooL/githubv4/LICENSE

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)