Skip to content

Commit 9fb710a

Browse files
feat: Add allow everyone option to GitHub OAuth2 logins (#5086)
* feat: Add allow everyone option for GitHub OAuth * fix: Detect team when multiple orgs are present Co-authored-by: 李董睿煊 <dongruixuan@hotmail.com>
1 parent f262fb4 commit 9fb710a

File tree

8 files changed

+187
-33
lines changed

8 files changed

+187
-33
lines changed

cli/deployment/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ func newConfig() *codersdk.DeploymentConfig {
185185
Usage: "Whether new users can sign up with GitHub.",
186186
Flag: "oauth2-github-allow-signups",
187187
},
188+
AllowEveryone: &codersdk.DeploymentConfigField[bool]{
189+
Name: "OAuth2 GitHub Allow Everyone",
190+
Usage: "Allow all logins, setting this option means allowed orgs and teams must be empty.",
191+
Flag: "oauth2-github-allow-everyone",
192+
},
188193
EnterpriseBaseURL: &codersdk.DeploymentConfigField[string]{
189194
Name: "OAuth2 GitHub Enterprise Base URL",
190195
Usage: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",

cli/server.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
375375
cfg.OAuth2.Github.ClientID.Value,
376376
cfg.OAuth2.Github.ClientSecret.Value,
377377
cfg.OAuth2.Github.AllowSignups.Value,
378+
cfg.OAuth2.Github.AllowEveryone.Value,
378379
cfg.OAuth2.Github.AllowedOrgs.Value,
379380
cfg.OAuth2.Github.AllowedTeams.Value,
380381
cfg.OAuth2.Github.EnterpriseBaseURL.Value,
@@ -1062,11 +1063,21 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles
10621063
return tlsConfig, nil
10631064
}
10641065

1065-
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
1066+
//nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive)
1067+
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
10661068
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
10671069
if err != nil {
10681070
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
10691071
}
1072+
if allowEveryone && len(allowOrgs) > 0 {
1073+
return nil, xerrors.New("allow everyone and allowed orgs cannot be used together")
1074+
}
1075+
if allowEveryone && len(rawTeams) > 0 {
1076+
return nil, xerrors.New("allow everyone and allowed teams cannot be used together")
1077+
}
1078+
if !allowEveryone && len(allowOrgs) == 0 {
1079+
return nil, xerrors.New("allowed orgs is empty: must specify at least one org or allow everyone")
1080+
}
10701081
allowTeams := make([]coderd.GithubOAuth2Team, 0, len(rawTeams))
10711082
for _, rawTeam := range rawTeams {
10721083
parts := strings.SplitN(rawTeam, "/", 2)
@@ -1118,6 +1129,7 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, al
11181129
},
11191130
},
11201131
AllowSignups: allowSignups,
1132+
AllowEveryone: allowEveryone,
11211133
AllowOrganizations: allowOrgs,
11221134
AllowTeams: allowTeams,
11231135
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {

cli/server_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ func TestServer(t *testing.T) {
608608
"--in-memory",
609609
"--address", ":0",
610610
"--access-url", "http://example.com",
611+
"--oauth2-github-allow-everyone",
611612
"--oauth2-github-client-id", "fake",
612613
"--oauth2-github-client-secret", "fake",
613614
"--oauth2-github-enterprise-base-url", fakeRedirect,

cli/testdata/coder_server_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ Flags:
6565
production.
6666
Consumes $CODER_EXPERIMENTAL
6767
-h, --help help for server
68+
--oauth2-github-allow-everyone Allow all logins, setting this option
69+
means allowed orgs and teams must be
70+
empty.
71+
Consumes $CODER_OAUTH2_GITHUB_ALLOW_EVERYONE
6872
--oauth2-github-allow-signups Whether new users can sign up with
6973
GitHub.
7074
Consumes $CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS

coderd/userauth.go

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type GithubOAuth2Config struct {
3838
TeamMembership func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error)
3939

4040
AllowSignups bool
41+
AllowEveryone bool
4142
AllowOrganizations []string
4243
AllowTeams []GithubOAuth2Team
4344
}
@@ -57,32 +58,38 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
5758
)
5859

5960
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(state.Token))
60-
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(ctx, oauthClient)
61-
if err != nil {
62-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
63-
Message: "Internal error fetching authenticated Github user organizations.",
64-
Detail: err.Error(),
65-
})
66-
return
67-
}
68-
var selectedMembership *github.Membership
69-
for _, membership := range memberships {
70-
if membership.GetState() != "active" {
71-
continue
61+
62+
var selectedMemberships []*github.Membership
63+
var organizationNames []string
64+
if !api.GithubOAuth2Config.AllowEveryone {
65+
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(ctx, oauthClient)
66+
if err != nil {
67+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
68+
Message: "Internal error fetching authenticated Github user organizations.",
69+
Detail: err.Error(),
70+
})
71+
return
7272
}
73-
for _, allowed := range api.GithubOAuth2Config.AllowOrganizations {
74-
if *membership.Organization.Login != allowed {
73+
74+
for _, membership := range memberships {
75+
if membership.GetState() != "active" {
7576
continue
7677
}
77-
selectedMembership = membership
78-
break
78+
for _, allowed := range api.GithubOAuth2Config.AllowOrganizations {
79+
if *membership.Organization.Login != allowed {
80+
continue
81+
}
82+
selectedMemberships = append(selectedMemberships, membership)
83+
organizationNames = append(organizationNames, membership.Organization.GetLogin())
84+
break
85+
}
86+
}
87+
if len(selectedMemberships) == 0 {
88+
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
89+
Message: "You aren't a member of the authorized Github organizations!",
90+
})
91+
return
7992
}
80-
}
81-
if selectedMembership == nil {
82-
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
83-
Message: "You aren't a member of the authorized Github organizations!",
84-
})
85-
return
8693
}
8794

8895
ghUser, err := api.GithubOAuth2Config.AuthenticatedUser(ctx, oauthClient)
@@ -95,24 +102,29 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
95102
}
96103

97104
// The default if no teams are specified is to allow all.
98-
if len(api.GithubOAuth2Config.AllowTeams) > 0 {
105+
if !api.GithubOAuth2Config.AllowEveryone && len(api.GithubOAuth2Config.AllowTeams) > 0 {
99106
var allowedTeam *github.Membership
100107
for _, allowTeam := range api.GithubOAuth2Config.AllowTeams {
101-
if allowTeam.Organization != *selectedMembership.Organization.Login {
102-
// This needs to continue because multiple organizations
103-
// could exist in the allow/team listings.
104-
continue
108+
if allowedTeam != nil {
109+
break
105110
}
111+
for _, selectedMembership := range selectedMemberships {
112+
if allowTeam.Organization != *selectedMembership.Organization.Login {
113+
// This needs to continue because multiple organizations
114+
// could exist in the allow/team listings.
115+
continue
116+
}
106117

107-
allowedTeam, err = api.GithubOAuth2Config.TeamMembership(ctx, oauthClient, allowTeam.Organization, allowTeam.Slug, *ghUser.Login)
108-
// The calling user may not have permission to the requested team!
109-
if err != nil {
110-
continue
118+
allowedTeam, err = api.GithubOAuth2Config.TeamMembership(ctx, oauthClient, allowTeam.Organization, allowTeam.Slug, *ghUser.Login)
119+
// The calling user may not have permission to the requested team!
120+
if err != nil {
121+
continue
122+
}
111123
}
112124
}
113125
if allowedTeam == nil {
114126
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
115-
Message: fmt.Sprintf("You aren't a member of an authorized team in the %s Github organization!", *selectedMembership.Organization.Login),
127+
Message: fmt.Sprintf("You aren't a member of an authorized team in the %v Github organization(s)!", organizationNames),
116128
})
117129
return
118130
}

coderd/userauth_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,124 @@ func TestUserOAuth2Github(t *testing.T) {
318318
resp := oauth2Callback(t, client)
319319
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
320320
})
321+
t.Run("SignupAllowedTeamInFirstOrganization", func(t *testing.T) {
322+
t.Parallel()
323+
client := coderdtest.New(t, &coderdtest.Options{
324+
GithubOAuth2Config: &coderd.GithubOAuth2Config{
325+
AllowSignups: true,
326+
AllowOrganizations: []string{"coder", "nil"},
327+
AllowTeams: []coderd.GithubOAuth2Team{{"coder", "backend"}},
328+
OAuth2Config: &oauth2Config{},
329+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
330+
return []*github.Membership{
331+
{
332+
State: &stateActive,
333+
Organization: &github.Organization{
334+
Login: github.String("coder"),
335+
},
336+
},
337+
{
338+
State: &stateActive,
339+
Organization: &github.Organization{
340+
Login: github.String("nil"),
341+
},
342+
},
343+
}, nil
344+
},
345+
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
346+
return &github.Membership{}, nil
347+
},
348+
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
349+
return &github.User{
350+
Login: github.String("mathias"),
351+
}, nil
352+
},
353+
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
354+
return []*github.UserEmail{{
355+
Email: github.String("mathias@coder.com"),
356+
Verified: github.Bool(true),
357+
Primary: github.Bool(true),
358+
}}, nil
359+
},
360+
},
361+
})
362+
resp := oauth2Callback(t, client)
363+
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
364+
})
365+
t.Run("SignupAllowedTeamInSecondOrganization", func(t *testing.T) {
366+
t.Parallel()
367+
client := coderdtest.New(t, &coderdtest.Options{
368+
GithubOAuth2Config: &coderd.GithubOAuth2Config{
369+
AllowSignups: true,
370+
AllowOrganizations: []string{"coder", "nil"},
371+
AllowTeams: []coderd.GithubOAuth2Team{{"nil", "null"}},
372+
OAuth2Config: &oauth2Config{},
373+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
374+
return []*github.Membership{
375+
{
376+
State: &stateActive,
377+
Organization: &github.Organization{
378+
Login: github.String("coder"),
379+
},
380+
},
381+
{
382+
State: &stateActive,
383+
Organization: &github.Organization{
384+
Login: github.String("nil"),
385+
},
386+
},
387+
}, nil
388+
},
389+
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
390+
return &github.Membership{}, nil
391+
},
392+
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
393+
return &github.User{
394+
Login: github.String("mathias"),
395+
}, nil
396+
},
397+
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
398+
return []*github.UserEmail{{
399+
Email: github.String("mathias@coder.com"),
400+
Verified: github.Bool(true),
401+
Primary: github.Bool(true),
402+
}}, nil
403+
},
404+
},
405+
})
406+
resp := oauth2Callback(t, client)
407+
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
408+
})
409+
t.Run("SignupAllowEveryone", func(t *testing.T) {
410+
t.Parallel()
411+
client := coderdtest.New(t, &coderdtest.Options{
412+
GithubOAuth2Config: &coderd.GithubOAuth2Config{
413+
AllowSignups: true,
414+
AllowEveryone: true,
415+
OAuth2Config: &oauth2Config{},
416+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
417+
return []*github.Membership{}, nil
418+
},
419+
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
420+
return nil, xerrors.New("no teams")
421+
},
422+
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
423+
return &github.User{
424+
Login: github.String("mathias"),
425+
}, nil
426+
},
427+
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
428+
return []*github.UserEmail{{
429+
Email: github.String("mathias@coder.com"),
430+
Verified: github.Bool(true),
431+
Primary: github.Bool(true),
432+
}}, nil
433+
},
434+
},
435+
})
436+
resp := oauth2Callback(t, client)
437+
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
438+
})
321439
t.Run("SignupFailedInactiveInOrg", func(t *testing.T) {
322440
t.Parallel()
323441
client := coderdtest.New(t, &coderdtest.Options{

codersdk/deploymentconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type OAuth2GithubConfig struct {
8181
AllowedOrgs *DeploymentConfigField[[]string] `json:"allowed_orgs" typescript:",notnull"`
8282
AllowedTeams *DeploymentConfigField[[]string] `json:"allowed_teams" typescript:",notnull"`
8383
AllowSignups *DeploymentConfigField[bool] `json:"allow_signups" typescript:",notnull"`
84+
AllowEveryone *DeploymentConfigField[bool] `json:"allow_everyone" typescript:",notnull"`
8485
EnterpriseBaseURL *DeploymentConfigField[string] `json:"enterprise_base_url" typescript:",notnull"`
8586
}
8687

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ export interface OAuth2GithubConfig {
433433
readonly allowed_orgs: DeploymentConfigField<string[]>
434434
readonly allowed_teams: DeploymentConfigField<string[]>
435435
readonly allow_signups: DeploymentConfigField<boolean>
436+
readonly allow_everyone: DeploymentConfigField<boolean>
436437
readonly enterprise_base_url: DeploymentConfigField<string>
437438
}
438439

0 commit comments

Comments
 (0)