diff --git a/cli/deployment/config.go b/cli/deployment/config.go index 1f309c7314..f6c8a1555b 100644 --- a/cli/deployment/config.go +++ b/cli/deployment/config.go @@ -185,6 +185,11 @@ func newConfig() *codersdk.DeploymentConfig { Usage: "Whether new users can sign up with GitHub.", 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]{ Name: "OAuth2 GitHub Enterprise Base URL", Usage: "Base URL of a GitHub Enterprise deployment to use for Login with GitHub.", diff --git a/cli/server.go b/cli/server.go index aa1cb3caee..4dfd4c5a40 100644 --- a/cli/server.go +++ b/cli/server.go @@ -375,6 +375,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co cfg.OAuth2.Github.ClientID.Value, cfg.OAuth2.Github.ClientSecret.Value, cfg.OAuth2.Github.AllowSignups.Value, + cfg.OAuth2.Github.AllowEveryone.Value, cfg.OAuth2.Github.AllowedOrgs.Value, cfg.OAuth2.Github.AllowedTeams.Value, cfg.OAuth2.Github.EnterpriseBaseURL.Value, @@ -1062,11 +1063,21 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles 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") if err != nil { 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)) for _, rawTeam := range rawTeams { parts := strings.SplitN(rawTeam, "/", 2) @@ -1118,6 +1129,7 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, al }, }, AllowSignups: allowSignups, + AllowEveryone: allowEveryone, AllowOrganizations: allowOrgs, AllowTeams: allowTeams, AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) { diff --git a/cli/server_test.go b/cli/server_test.go index ec3e3643fe..30356cd18c 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -608,6 +608,7 @@ func TestServer(t *testing.T) { "--in-memory", "--address", ":0", "--access-url", "http://example.com", + "--oauth2-github-allow-everyone", "--oauth2-github-client-id", "fake", "--oauth2-github-client-secret", "fake", "--oauth2-github-enterprise-base-url", fakeRedirect, diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 3a9e6122e1..f609657cc5 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -65,6 +65,10 @@ Flags: production. Consumes $CODER_EXPERIMENTAL -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 GitHub. Consumes $CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS diff --git a/coderd/userauth.go b/coderd/userauth.go index eaec92f0ab..deffdca9c3 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -38,6 +38,7 @@ type GithubOAuth2Config struct { TeamMembership func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error) AllowSignups bool + AllowEveryone bool AllowOrganizations []string AllowTeams []GithubOAuth2Team } @@ -57,32 +58,38 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { ) oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(state.Token)) - memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(ctx, oauthClient) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching authenticated Github user organizations.", - Detail: err.Error(), - }) - return - } - var selectedMembership *github.Membership - for _, membership := range memberships { - if membership.GetState() != "active" { - continue + + var selectedMemberships []*github.Membership + var organizationNames []string + if !api.GithubOAuth2Config.AllowEveryone { + memberships, err := api.GithubOAuth2Config.ListOrganizationMemberships(ctx, oauthClient) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching authenticated Github user organizations.", + Detail: err.Error(), + }) + return } - for _, allowed := range api.GithubOAuth2Config.AllowOrganizations { - if *membership.Organization.Login != allowed { + + for _, membership := range memberships { + if membership.GetState() != "active" { continue } - selectedMembership = membership - break + for _, allowed := range api.GithubOAuth2Config.AllowOrganizations { + if *membership.Organization.Login != allowed { + continue + } + selectedMemberships = append(selectedMemberships, membership) + organizationNames = append(organizationNames, membership.Organization.GetLogin()) + break + } + } + if len(selectedMemberships) == 0 { + httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ + Message: "You aren't a member of the authorized Github organizations!", + }) + return } - } - if selectedMembership == nil { - httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ - Message: "You aren't a member of the authorized Github organizations!", - }) - return } ghUser, err := api.GithubOAuth2Config.AuthenticatedUser(ctx, oauthClient) @@ -95,24 +102,29 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) { } // 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 for _, allowTeam := range api.GithubOAuth2Config.AllowTeams { - if allowTeam.Organization != *selectedMembership.Organization.Login { - // This needs to continue because multiple organizations - // could exist in the allow/team listings. - continue + if allowedTeam != nil { + break } + for _, selectedMembership := range selectedMemberships { + if allowTeam.Organization != *selectedMembership.Organization.Login { + // This needs to continue because multiple organizations + // could exist in the allow/team listings. + continue + } - allowedTeam, err = api.GithubOAuth2Config.TeamMembership(ctx, oauthClient, allowTeam.Organization, allowTeam.Slug, *ghUser.Login) - // The calling user may not have permission to the requested team! - if err != nil { - continue + allowedTeam, err = api.GithubOAuth2Config.TeamMembership(ctx, oauthClient, allowTeam.Organization, allowTeam.Slug, *ghUser.Login) + // The calling user may not have permission to the requested team! + if err != nil { + continue + } } } if allowedTeam == nil { 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 } diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 2f9751cc61..04abffedaa 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -318,6 +318,124 @@ func TestUserOAuth2Github(t *testing.T) { resp := oauth2Callback(t, client) 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.Parallel() client := coderdtest.New(t, &coderdtest.Options{ diff --git a/codersdk/deploymentconfig.go b/codersdk/deploymentconfig.go index cb56ba5aa8..5b06d0303e 100644 --- a/codersdk/deploymentconfig.go +++ b/codersdk/deploymentconfig.go @@ -81,6 +81,7 @@ type OAuth2GithubConfig struct { AllowedOrgs *DeploymentConfigField[[]string] `json:"allowed_orgs" typescript:",notnull"` AllowedTeams *DeploymentConfigField[[]string] `json:"allowed_teams" 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"` } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d92fd82213..ddd887fb54 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -433,6 +433,7 @@ export interface OAuth2GithubConfig { readonly allowed_orgs: DeploymentConfigField readonly allowed_teams: DeploymentConfigField readonly allow_signups: DeploymentConfigField + readonly allow_everyone: DeploymentConfigField readonly enterprise_base_url: DeploymentConfigField }