Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: Add allow everyone option to GitHub OAuth2 logins #5086

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Add allow everyone option for GitHub OAuth
  • Loading branch information
mafredri committed Nov 15, 2022
commit 67387aba997e681cedee316994db85b545b9e347
5 changes: 5 additions & 0 deletions cli/deployment/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 must be empty and no org limits will be imposed.",
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.",
Expand Down
13 changes: 12 additions & 1 deletion cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1062,11 +1063,20 @@ 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) {
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)
Expand Down Expand Up @@ -1118,6 +1128,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) {
Expand Down
55 changes: 32 additions & 23 deletions coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -57,17 +58,19 @@ 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
}
// The default if no organizations are specified is to allow all.
var selectedMembership *github.Membership
if len(api.GithubOAuth2Config.AllowOrganizations) > 0 {

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 _, membership := range memberships {
if membership.GetState() != "active" {
continue
Expand All @@ -76,11 +79,12 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
if *membership.Organization.Login != allowed {
continue
}
selectedMembership = membership
selectedMemberships = append(selectedMemberships, membership)
organizationNames = append(organizationNames, membership.Organization.GetLogin())
break
}
}
if selectedMembership == nil {
if len(selectedMemberships) == 0 {
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
Message: "You aren't a member of the authorized Github organizations!",
})
Expand All @@ -98,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.AllowOrganizations) > 0 && 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
}
Expand Down
118 changes: 118 additions & 0 deletions coderd/userauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("[email protected]"),
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("[email protected]"),
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("[email protected]"),
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{
Expand Down
1 change: 1 addition & 0 deletions codersdk/deploymentconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export interface OAuth2GithubConfig {
readonly allowed_orgs: DeploymentConfigField<string[]>
readonly allowed_teams: DeploymentConfigField<string[]>
readonly allow_signups: DeploymentConfigField<boolean>
readonly allow_everyone: DeploymentConfigField<boolean>
readonly enterprise_base_url: DeploymentConfigField<string>
}

Expand Down