github: use GraphQL API to reduce number of API calls for directory sync (#2715)

* github: use GraphQL API to reduce number of API calls for directory sync

* fix id encoding

* github: use slug instead of id, update upgrading.md

* Update docs/docs/upgrading.md

Co-authored-by: Alex Fornuto <afornuto@pomerium.com>

Co-authored-by: Alex Fornuto <afornuto@pomerium.com>
This commit is contained in:
Caleb Doxsey 2021-10-27 11:50:48 -06:00 committed by GitHub
parent d390e80b30
commit 99b905a336
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 499 additions and 204 deletions

View file

@ -11,6 +11,8 @@ import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/stretchr/testify/assert"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/parser"
"github.com/pomerium/pomerium/internal/testutil"
)
@ -35,26 +37,198 @@ func newMockAPI(t *testing.T, srv *httptest.Server) http.Handler {
}
json.NewDecoder(r.Body).Decode(&body)
_ = json.NewEncoder(w).Encode(M{
"data": M{
"organization": M{
"teams": M{
"totalCount": 3,
"edges": []M{
{"node": M{
"id": "MDQ6VGVhbTE=",
}},
{"node": M{
"id": "MDQ6VGVhbTI=",
}},
{"node": M{
"id": "MDQ6VGVhbTM=",
}},
},
},
},
},
q, err := parser.ParseQuery(&ast.Source{
Input: body.Query,
})
if err != nil {
panic(err)
}
result := qlResult{
Data: &qlData{
Organization: &qlOrganization{},
},
}
handleMembersWithRole := func(orgSlug string, field *ast.Field) {
membersWithRole := &qlMembersWithRoleConnection{}
var cursor string
for _, arg := range field.Arguments {
if arg.Name == "after" {
cursor = arg.Value.Raw
}
}
switch cursor {
case `null`:
switch orgSlug {
case "org1":
membersWithRole.PageInfo = qlPageInfo{EndCursor: "TOKEN1", HasNextPage: true}
membersWithRole.Nodes = []qlUser{
{ID: "user1", Login: "user1", Name: "User 1", Email: "user1@example.com"},
{ID: "user2", Login: "user2", Name: "User 2", Email: "user2@example.com"},
}
case "org2":
membersWithRole.PageInfo = qlPageInfo{HasNextPage: false}
membersWithRole.Nodes = []qlUser{
{ID: "user4", Login: "user4", Name: "User 4", Email: "user4@example.com"},
}
default:
t.Errorf("unexpected org slug: %s", orgSlug)
}
case `TOKEN1`:
membersWithRole.PageInfo = qlPageInfo{HasNextPage: false}
membersWithRole.Nodes = []qlUser{
{ID: "user3", Login: "user3", Name: "User 3", Email: "user3@example.com"},
}
default:
t.Errorf("unexpected cursor: %s", cursor)
}
result.Data.Organization.MembersWithRole = membersWithRole
}
handleTeamMembers := func(orgSlug, teamSlug string, field *ast.Field) {
result.Data.Organization.Team.Members = &qlTeamMemberConnection{
PageInfo: qlPageInfo{HasNextPage: false},
}
switch teamSlug {
case "team3":
result.Data.Organization.Team.Members.Edges = []qlTeamMemberEdge{
{Node: qlUser{ID: "user3"}},
}
}
}
handleTeam := func(orgSlug string, field *ast.Field) {
result.Data.Organization.Team = &qlTeam{}
var teamSlug string
for _, arg := range field.Arguments {
if arg.Name == "slug" {
teamSlug = arg.Value.Raw
}
}
for _, selection := range field.SelectionSet {
subField, ok := selection.(*ast.Field)
if !ok {
continue
}
switch subField.Name {
case "members":
handleTeamMembers(orgSlug, teamSlug, subField)
}
}
}
handleTeams := func(orgSlug string, field *ast.Field) {
teams := &qlTeamConnection{}
var cursor string
var userLogin string
for _, arg := range field.Arguments {
if arg.Name == "after" {
cursor = arg.Value.Raw
}
if arg.Name == "userLogins" {
userLogin = arg.Value.Children[0].Value.Raw
}
}
switch cursor {
case `null`:
switch orgSlug {
case "org1":
teams.PageInfo = qlPageInfo{HasNextPage: true, EndCursor: "TOKEN1"}
teams.Edges = []qlTeamEdge{
{Node: qlTeam{ID: "MDQ6VGVhbTE=", Slug: "team1", Name: "Team 1", Members: &qlTeamMemberConnection{
PageInfo: qlPageInfo{HasNextPage: false},
Edges: []qlTeamMemberEdge{
{Node: qlUser{ID: "user1"}},
{Node: qlUser{ID: "user2"}},
},
}}},
}
case "org2":
teams.PageInfo = qlPageInfo{HasNextPage: false}
teams.Edges = []qlTeamEdge{
{Node: qlTeam{ID: "MDQ6VGVhbTM=", Slug: "team3", Name: "Team 3", Members: &qlTeamMemberConnection{
PageInfo: qlPageInfo{HasNextPage: true, EndCursor: "TOKEN1"},
Edges: []qlTeamMemberEdge{
{Node: qlUser{ID: "user1"}},
{Node: qlUser{ID: "user2"}},
},
}}},
}
if userLogin == "" || userLogin == "user4" {
teams.Edges = append(teams.Edges, qlTeamEdge{
Node: qlTeam{ID: "MDQ6VGVhbTQ=", Slug: "team4", Name: "Team 4", Members: &qlTeamMemberConnection{
PageInfo: qlPageInfo{HasNextPage: false},
Edges: []qlTeamMemberEdge{
{Node: qlUser{ID: "user4"}},
},
}},
})
}
default:
t.Errorf("unexpected org slug: %s", orgSlug)
}
case "TOKEN1":
teams.PageInfo = qlPageInfo{HasNextPage: false}
teams.Edges = []qlTeamEdge{
{Node: qlTeam{ID: "MDQ6VGVhbTI=", Slug: "team2", Name: "Team 2", Members: &qlTeamMemberConnection{
PageInfo: qlPageInfo{HasNextPage: false},
Edges: []qlTeamMemberEdge{
{Node: qlUser{ID: "user1"}},
},
}}},
}
default:
t.Errorf("unexpected cursor: %s", cursor)
}
result.Data.Organization.Teams = teams
}
handleOrganization := func(field *ast.Field) {
var orgSlug string
for _, arg := range field.Arguments {
if arg.Name == "login" {
orgSlug = arg.Value.Raw
}
}
for _, orgSelection := range field.SelectionSet {
orgField, ok := orgSelection.(*ast.Field)
if !ok {
continue
}
switch orgField.Name {
case "teams":
handleTeams(orgSlug, orgField)
case "team":
handleTeam(orgSlug, orgField)
case "membersWithRole":
handleMembersWithRole(orgSlug, orgField)
}
}
}
for _, operation := range q.Operations {
for _, selection := range operation.SelectionSet {
field, ok := selection.(*ast.Field)
if !ok {
continue
}
if field.Name != "organization" {
continue
}
handleOrganization(field)
}
}
_ = json.NewEncoder(w).Encode(result)
})
r.Get("/user/orgs", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]M{
@ -136,7 +310,7 @@ func TestProvider_User(t *testing.T) {
}
testutil.AssertProtoJSONEqual(t, `{
"id": "user1",
"groupIds": ["1", "2", "3"],
"groupIds": ["team1", "team2", "team3"],
"displayName": "User 1",
"email": "user1@example.com"
}`, du)
@ -160,16 +334,16 @@ func TestProvider_UserGroups(t *testing.T) {
groups, users, err := p.UserGroups(context.Background())
assert.NoError(t, err)
testutil.AssertProtoJSONEqual(t, `[
{ "id": "user1", "groupIds": ["1", "2", "3"], "displayName": "User 1", "email": "user1@example.com" },
{ "id": "user2", "groupIds": ["1", "3"], "displayName": "User 2", "email": "user2@example.com" },
{ "id": "user3", "groupIds": ["3"], "displayName": "User 3", "email": "user3@example.com" },
{ "id": "user4", "groupIds": ["4"], "displayName": "User 4", "email": "user4@example.com" }
{ "id": "user1", "groupIds": ["team1", "team2", "team3"], "displayName": "User 1", "email": "user1@example.com" },
{ "id": "user2", "groupIds": ["team1", "team3"], "displayName": "User 2", "email": "user2@example.com" },
{ "id": "user3", "groupIds": ["team3"], "displayName": "User 3", "email": "user3@example.com" },
{ "id": "user4", "groupIds": ["team4"], "displayName": "User 4", "email": "user4@example.com" }
]`, users)
testutil.AssertProtoJSONEqual(t, `[
{ "id": "1", "name": "team1" },
{ "id": "2", "name": "team2" },
{ "id": "3", "name": "team3" },
{ "id": "4", "name": "team4" }
{ "id": "team1", "name": "team1" },
{ "id": "team2", "name": "team2" },
{ "id": "team3", "name": "team3" },
{ "id": "team4", "name": "team4" }
]`, groups)
}