mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
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>
This commit is contained in:
committed by
GitHub
parent
f262fb4811
commit
9fb710a04f
@ -185,6 +185,11 @@ func newConfig() *codersdk.DeploymentConfig {
|
|||||||
Usage: "Whether new users can sign up with GitHub.",
|
Usage: "Whether new users can sign up with GitHub.",
|
||||||
Flag: "oauth2-github-allow-signups",
|
Flag: "oauth2-github-allow-signups",
|
||||||
},
|
},
|
||||||
|
AllowEveryone: &codersdk.DeploymentConfigField[bool]{
|
||||||
|
Name: "OAuth2 GitHub Allow Everyone",
|
||||||
|
Usage: "Allow all logins, setting this option means allowed orgs and teams must be empty.",
|
||||||
|
Flag: "oauth2-github-allow-everyone",
|
||||||
|
},
|
||||||
EnterpriseBaseURL: &codersdk.DeploymentConfigField[string]{
|
EnterpriseBaseURL: &codersdk.DeploymentConfigField[string]{
|
||||||
Name: "OAuth2 GitHub Enterprise Base URL",
|
Name: "OAuth2 GitHub Enterprise Base URL",
|
||||||
Usage: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
|
Usage: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.",
|
||||||
|
@ -375,6 +375,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
|
|||||||
cfg.OAuth2.Github.ClientID.Value,
|
cfg.OAuth2.Github.ClientID.Value,
|
||||||
cfg.OAuth2.Github.ClientSecret.Value,
|
cfg.OAuth2.Github.ClientSecret.Value,
|
||||||
cfg.OAuth2.Github.AllowSignups.Value,
|
cfg.OAuth2.Github.AllowSignups.Value,
|
||||||
|
cfg.OAuth2.Github.AllowEveryone.Value,
|
||||||
cfg.OAuth2.Github.AllowedOrgs.Value,
|
cfg.OAuth2.Github.AllowedOrgs.Value,
|
||||||
cfg.OAuth2.Github.AllowedTeams.Value,
|
cfg.OAuth2.Github.AllowedTeams.Value,
|
||||||
cfg.OAuth2.Github.EnterpriseBaseURL.Value,
|
cfg.OAuth2.Github.EnterpriseBaseURL.Value,
|
||||||
@ -1062,11 +1063,21 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles
|
|||||||
return tlsConfig, nil
|
return tlsConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
|
//nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive)
|
||||||
|
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
|
||||||
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
|
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
|
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
|
||||||
}
|
}
|
||||||
|
if allowEveryone && len(allowOrgs) > 0 {
|
||||||
|
return nil, xerrors.New("allow everyone and allowed orgs cannot be used together")
|
||||||
|
}
|
||||||
|
if allowEveryone && len(rawTeams) > 0 {
|
||||||
|
return nil, xerrors.New("allow everyone and allowed teams cannot be used together")
|
||||||
|
}
|
||||||
|
if !allowEveryone && len(allowOrgs) == 0 {
|
||||||
|
return nil, xerrors.New("allowed orgs is empty: must specify at least one org or allow everyone")
|
||||||
|
}
|
||||||
allowTeams := make([]coderd.GithubOAuth2Team, 0, len(rawTeams))
|
allowTeams := make([]coderd.GithubOAuth2Team, 0, len(rawTeams))
|
||||||
for _, rawTeam := range rawTeams {
|
for _, rawTeam := range rawTeams {
|
||||||
parts := strings.SplitN(rawTeam, "/", 2)
|
parts := strings.SplitN(rawTeam, "/", 2)
|
||||||
@ -1118,6 +1129,7 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, al
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
AllowSignups: allowSignups,
|
AllowSignups: allowSignups,
|
||||||
|
AllowEveryone: allowEveryone,
|
||||||
AllowOrganizations: allowOrgs,
|
AllowOrganizations: allowOrgs,
|
||||||
AllowTeams: allowTeams,
|
AllowTeams: allowTeams,
|
||||||
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
|
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
|
||||||
|
@ -608,6 +608,7 @@ func TestServer(t *testing.T) {
|
|||||||
"--in-memory",
|
"--in-memory",
|
||||||
"--address", ":0",
|
"--address", ":0",
|
||||||
"--access-url", "http://example.com",
|
"--access-url", "http://example.com",
|
||||||
|
"--oauth2-github-allow-everyone",
|
||||||
"--oauth2-github-client-id", "fake",
|
"--oauth2-github-client-id", "fake",
|
||||||
"--oauth2-github-client-secret", "fake",
|
"--oauth2-github-client-secret", "fake",
|
||||||
"--oauth2-github-enterprise-base-url", fakeRedirect,
|
"--oauth2-github-enterprise-base-url", fakeRedirect,
|
||||||
|
4
cli/testdata/coder_server_--help.golden
vendored
4
cli/testdata/coder_server_--help.golden
vendored
@ -65,6 +65,10 @@ Flags:
|
|||||||
production.
|
production.
|
||||||
Consumes $CODER_EXPERIMENTAL
|
Consumes $CODER_EXPERIMENTAL
|
||||||
-h, --help help for server
|
-h, --help help for server
|
||||||
|
--oauth2-github-allow-everyone Allow all logins, setting this option
|
||||||
|
means allowed orgs and teams must be
|
||||||
|
empty.
|
||||||
|
Consumes $CODER_OAUTH2_GITHUB_ALLOW_EVERYONE
|
||||||
--oauth2-github-allow-signups Whether new users can sign up with
|
--oauth2-github-allow-signups Whether new users can sign up with
|
||||||
GitHub.
|
GitHub.
|
||||||
Consumes $CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS
|
Consumes $CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS
|
||||||
|
@ -38,6 +38,7 @@ type GithubOAuth2Config struct {
|
|||||||
TeamMembership func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error)
|
TeamMembership func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error)
|
||||||
|
|
||||||
AllowSignups bool
|
AllowSignups bool
|
||||||
|
AllowEveryone bool
|
||||||
AllowOrganizations []string
|
AllowOrganizations []string
|
||||||
AllowTeams []GithubOAuth2Team
|
AllowTeams []GithubOAuth2Team
|
||||||
}
|
}
|
||||||
@ -57,6 +58,10 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(state.Token))
|
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(state.Token))
|
||||||
|
|
||||||
|
var selectedMemberships []*github.Membership
|
||||||
|
var organizationNames []string
|
||||||
|
if !api.GithubOAuth2Config.AllowEveryone {
|
||||||
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(ctx, oauthClient)
|
memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(ctx, oauthClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
@ -65,7 +70,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var selectedMembership *github.Membership
|
|
||||||
for _, membership := range memberships {
|
for _, membership := range memberships {
|
||||||
if membership.GetState() != "active" {
|
if membership.GetState() != "active" {
|
||||||
continue
|
continue
|
||||||
@ -74,16 +79,18 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
|||||||
if *membership.Organization.Login != allowed {
|
if *membership.Organization.Login != allowed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
selectedMembership = membership
|
selectedMemberships = append(selectedMemberships, membership)
|
||||||
|
organizationNames = append(organizationNames, membership.Organization.GetLogin())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if selectedMembership == nil {
|
if len(selectedMemberships) == 0 {
|
||||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
||||||
Message: "You aren't a member of the authorized Github organizations!",
|
Message: "You aren't a member of the authorized Github organizations!",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ghUser, err := api.GithubOAuth2Config.AuthenticatedUser(ctx, oauthClient)
|
ghUser, err := api.GithubOAuth2Config.AuthenticatedUser(ctx, oauthClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -95,9 +102,13 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The default if no teams are specified is to allow all.
|
// The default if no teams are specified is to allow all.
|
||||||
if len(api.GithubOAuth2Config.AllowTeams) > 0 {
|
if !api.GithubOAuth2Config.AllowEveryone && len(api.GithubOAuth2Config.AllowTeams) > 0 {
|
||||||
var allowedTeam *github.Membership
|
var allowedTeam *github.Membership
|
||||||
for _, allowTeam := range api.GithubOAuth2Config.AllowTeams {
|
for _, allowTeam := range api.GithubOAuth2Config.AllowTeams {
|
||||||
|
if allowedTeam != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, selectedMembership := range selectedMemberships {
|
||||||
if allowTeam.Organization != *selectedMembership.Organization.Login {
|
if allowTeam.Organization != *selectedMembership.Organization.Login {
|
||||||
// This needs to continue because multiple organizations
|
// This needs to continue because multiple organizations
|
||||||
// could exist in the allow/team listings.
|
// could exist in the allow/team listings.
|
||||||
@ -110,9 +121,10 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if allowedTeam == nil {
|
if allowedTeam == nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
|
||||||
Message: fmt.Sprintf("You aren't a member of an authorized team in the %s Github organization!", *selectedMembership.Organization.Login),
|
Message: fmt.Sprintf("You aren't a member of an authorized team in the %v Github organization(s)!", organizationNames),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -318,6 +318,124 @@ func TestUserOAuth2Github(t *testing.T) {
|
|||||||
resp := oauth2Callback(t, client)
|
resp := oauth2Callback(t, client)
|
||||||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
t.Run("SignupAllowedTeamInFirstOrganization", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
GithubOAuth2Config: &coderd.GithubOAuth2Config{
|
||||||
|
AllowSignups: true,
|
||||||
|
AllowOrganizations: []string{"coder", "nil"},
|
||||||
|
AllowTeams: []coderd.GithubOAuth2Team{{"coder", "backend"}},
|
||||||
|
OAuth2Config: &oauth2Config{},
|
||||||
|
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
|
||||||
|
return []*github.Membership{
|
||||||
|
{
|
||||||
|
State: &stateActive,
|
||||||
|
Organization: &github.Organization{
|
||||||
|
Login: github.String("coder"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: &stateActive,
|
||||||
|
Organization: &github.Organization{
|
||||||
|
Login: github.String("nil"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
|
||||||
|
return &github.Membership{}, nil
|
||||||
|
},
|
||||||
|
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
|
||||||
|
return &github.User{
|
||||||
|
Login: github.String("mathias"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
|
||||||
|
return []*github.UserEmail{{
|
||||||
|
Email: github.String("mathias@coder.com"),
|
||||||
|
Verified: github.Bool(true),
|
||||||
|
Primary: github.Bool(true),
|
||||||
|
}}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
resp := oauth2Callback(t, client)
|
||||||
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||||
|
})
|
||||||
|
t.Run("SignupAllowedTeamInSecondOrganization", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
GithubOAuth2Config: &coderd.GithubOAuth2Config{
|
||||||
|
AllowSignups: true,
|
||||||
|
AllowOrganizations: []string{"coder", "nil"},
|
||||||
|
AllowTeams: []coderd.GithubOAuth2Team{{"nil", "null"}},
|
||||||
|
OAuth2Config: &oauth2Config{},
|
||||||
|
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
|
||||||
|
return []*github.Membership{
|
||||||
|
{
|
||||||
|
State: &stateActive,
|
||||||
|
Organization: &github.Organization{
|
||||||
|
Login: github.String("coder"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: &stateActive,
|
||||||
|
Organization: &github.Organization{
|
||||||
|
Login: github.String("nil"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
|
||||||
|
return &github.Membership{}, nil
|
||||||
|
},
|
||||||
|
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
|
||||||
|
return &github.User{
|
||||||
|
Login: github.String("mathias"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
|
||||||
|
return []*github.UserEmail{{
|
||||||
|
Email: github.String("mathias@coder.com"),
|
||||||
|
Verified: github.Bool(true),
|
||||||
|
Primary: github.Bool(true),
|
||||||
|
}}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
resp := oauth2Callback(t, client)
|
||||||
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||||
|
})
|
||||||
|
t.Run("SignupAllowEveryone", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
GithubOAuth2Config: &coderd.GithubOAuth2Config{
|
||||||
|
AllowSignups: true,
|
||||||
|
AllowEveryone: true,
|
||||||
|
OAuth2Config: &oauth2Config{},
|
||||||
|
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
|
||||||
|
return []*github.Membership{}, nil
|
||||||
|
},
|
||||||
|
TeamMembership: func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) {
|
||||||
|
return nil, xerrors.New("no teams")
|
||||||
|
},
|
||||||
|
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
|
||||||
|
return &github.User{
|
||||||
|
Login: github.String("mathias"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
|
||||||
|
return []*github.UserEmail{{
|
||||||
|
Email: github.String("mathias@coder.com"),
|
||||||
|
Verified: github.Bool(true),
|
||||||
|
Primary: github.Bool(true),
|
||||||
|
}}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
resp := oauth2Callback(t, client)
|
||||||
|
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||||
|
})
|
||||||
t.Run("SignupFailedInactiveInOrg", func(t *testing.T) {
|
t.Run("SignupFailedInactiveInOrg", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, &coderdtest.Options{
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
@ -81,6 +81,7 @@ type OAuth2GithubConfig struct {
|
|||||||
AllowedOrgs *DeploymentConfigField[[]string] `json:"allowed_orgs" typescript:",notnull"`
|
AllowedOrgs *DeploymentConfigField[[]string] `json:"allowed_orgs" typescript:",notnull"`
|
||||||
AllowedTeams *DeploymentConfigField[[]string] `json:"allowed_teams" typescript:",notnull"`
|
AllowedTeams *DeploymentConfigField[[]string] `json:"allowed_teams" typescript:",notnull"`
|
||||||
AllowSignups *DeploymentConfigField[bool] `json:"allow_signups" typescript:",notnull"`
|
AllowSignups *DeploymentConfigField[bool] `json:"allow_signups" typescript:",notnull"`
|
||||||
|
AllowEveryone *DeploymentConfigField[bool] `json:"allow_everyone" typescript:",notnull"`
|
||||||
EnterpriseBaseURL *DeploymentConfigField[string] `json:"enterprise_base_url" typescript:",notnull"`
|
EnterpriseBaseURL *DeploymentConfigField[string] `json:"enterprise_base_url" typescript:",notnull"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,6 +433,7 @@ export interface OAuth2GithubConfig {
|
|||||||
readonly allowed_orgs: DeploymentConfigField<string[]>
|
readonly allowed_orgs: DeploymentConfigField<string[]>
|
||||||
readonly allowed_teams: DeploymentConfigField<string[]>
|
readonly allowed_teams: DeploymentConfigField<string[]>
|
||||||
readonly allow_signups: DeploymentConfigField<boolean>
|
readonly allow_signups: DeploymentConfigField<boolean>
|
||||||
|
readonly allow_everyone: DeploymentConfigField<boolean>
|
||||||
readonly enterprise_base_url: DeploymentConfigField<string>
|
readonly enterprise_base_url: DeploymentConfigField<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user