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

Skip to content

Commit dff6e97

Browse files
authored
feat: Add allowlist of GitHub teams for OAuth (coder#2849)
Fixes coder#2848.
1 parent c801da4 commit dff6e97

File tree

5 files changed

+134
-3
lines changed

5 files changed

+134
-3
lines changed

cli/cliflag/cliflag.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func StringArrayVarP(flagset *pflag.FlagSet, ptr *[]string, name string, shortha
4747
def = strings.Split(val, ",")
4848
}
4949
}
50-
flagset.StringArrayVarP(ptr, name, shorthand, def, usage)
50+
flagset.StringArrayVarP(ptr, name, shorthand, def, fmtUsage(usage, env))
5151
}
5252

5353
// Uint8VarP sets a uint8 flag on the given flag set.

cli/server.go

+26-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func server() *cobra.Command {
8282
oauth2GithubClientID string
8383
oauth2GithubClientSecret string
8484
oauth2GithubAllowedOrganizations []string
85+
oauth2GithubAllowedTeams []string
8586
oauth2GithubAllowSignups bool
8687
telemetryEnable bool
8788
telemetryURL string
@@ -264,7 +265,7 @@ func server() *cobra.Command {
264265
}
265266

266267
if oauth2GithubClientSecret != "" {
267-
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed, oauth2GithubClientID, oauth2GithubClientSecret, oauth2GithubAllowSignups, oauth2GithubAllowedOrganizations)
268+
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed, oauth2GithubClientID, oauth2GithubClientSecret, oauth2GithubAllowSignups, oauth2GithubAllowedOrganizations, oauth2GithubAllowedTeams)
268269
if err != nil {
269270
return xerrors.Errorf("configure github oauth2: %w", err)
270271
}
@@ -535,6 +536,8 @@ func server() *cobra.Command {
535536
"Specifies a client secret to use for oauth2 with GitHub.")
536537
cliflag.StringArrayVarP(root.Flags(), &oauth2GithubAllowedOrganizations, "oauth2-github-allowed-orgs", "", "CODER_OAUTH2_GITHUB_ALLOWED_ORGS", nil,
537538
"Specifies organizations the user must be a member of to authenticate with GitHub.")
539+
cliflag.StringArrayVarP(root.Flags(), &oauth2GithubAllowedTeams, "oauth2-github-allowed-teams", "", "CODER_OAUTH2_GITHUB_ALLOWED_TEAMS", nil,
540+
"Specifies teams inside organizations the user must be a member of to authenticate with GitHub. Formatted as: <organization-name>/<team-slug>.")
538541
cliflag.BoolVarP(root.Flags(), &oauth2GithubAllowSignups, "oauth2-github-allow-signups", "", "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS", false,
539542
"Specifies whether new users can sign up with GitHub.")
540543
cliflag.BoolVarP(root.Flags(), &telemetryEnable, "telemetry", "", "CODER_TELEMETRY", true, "Specifies whether telemetry is enabled or not. Coder collects anonymized usage data to help improve our product.")
@@ -719,11 +722,22 @@ func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFi
719722
return tls.NewListener(listener, tlsConfig), nil
720723
}
721724

722-
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups bool, allowOrgs []string) (*coderd.GithubOAuth2Config, error) {
725+
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups bool, allowOrgs []string, rawTeams []string) (*coderd.GithubOAuth2Config, error) {
723726
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
724727
if err != nil {
725728
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
726729
}
730+
allowTeams := make([]coderd.GithubOAuth2Team, 0, len(rawTeams))
731+
for _, rawTeam := range rawTeams {
732+
parts := strings.SplitN(rawTeam, "/", 2)
733+
if len(parts) != 2 {
734+
return nil, xerrors.Errorf("github team allowlist is formatted incorrectly. got %s; wanted <organization>/<team>", rawTeam)
735+
}
736+
allowTeams = append(allowTeams, coderd.GithubOAuth2Team{
737+
Organization: parts[0],
738+
Slug: parts[1],
739+
})
740+
}
727741
return &coderd.GithubOAuth2Config{
728742
OAuth2Config: &oauth2.Config{
729743
ClientID: clientID,
@@ -738,6 +752,7 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, al
738752
},
739753
AllowSignups: allowSignups,
740754
AllowOrganizations: allowOrgs,
755+
AllowTeams: allowTeams,
741756
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
742757
user, _, err := github.NewClient(client).Users.Get(ctx, "")
743758
return user, err
@@ -749,9 +764,18 @@ func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, al
749764
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
750765
memberships, _, err := github.NewClient(client).Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{
751766
State: "active",
767+
ListOptions: github.ListOptions{
768+
PerPage: 100,
769+
},
752770
})
753771
return memberships, err
754772
},
773+
ListTeams: func(ctx context.Context, client *http.Client, org string) ([]*github.Team, error) {
774+
teams, _, err := github.NewClient(client).Teams.ListTeams(ctx, org, &github.ListOptions{
775+
PerPage: 100,
776+
})
777+
return teams, err
778+
},
755779
}, nil
756780
}
757781

coderd/database/db_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import (
1414

1515
func TestNestedInTx(t *testing.T) {
1616
t.Parallel()
17+
if testing.Short() {
18+
t.SkipNow()
19+
}
1720

1821
uid := uuid.New()
1922
sqlDB := testSQLDB(t)

coderd/userauth.go

+43
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@ import (
1717
"github.com/coder/coder/codersdk"
1818
)
1919

20+
// GithubOAuth2Team represents a team scoped to an organization.
21+
type GithubOAuth2Team struct {
22+
Organization string
23+
Slug string
24+
}
25+
2026
// GithubOAuth2Provider exposes required functions for the Github authentication flow.
2127
type GithubOAuth2Config struct {
2228
httpmw.OAuth2Config
2329
AuthenticatedUser func(ctx context.Context, client *http.Client) (*github.User, error)
2430
ListEmails func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error)
2531
ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error)
32+
ListTeams func(ctx context.Context, client *http.Client, org string) ([]*github.Team, error)
2633

2734
AllowSignups bool
2835
AllowOrganizations []string
36+
AllowTeams []GithubOAuth2Team
2937
}
3038

3139
func (api *API) userAuthMethods(rw http.ResponseWriter, _ *http.Request) {
@@ -64,6 +72,41 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
6472
return
6573
}
6674

75+
// The default if no teams are specified is to allow all.
76+
if len(api.GithubOAuth2Config.AllowTeams) > 0 {
77+
teams, err := api.GithubOAuth2Config.ListTeams(r.Context(), oauthClient, *selectedMembership.Organization.Login)
78+
if err != nil {
79+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
80+
Message: "Failed to fetch teams from GitHub.",
81+
Detail: err.Error(),
82+
})
83+
return
84+
}
85+
86+
var allowedTeam *github.Team
87+
for _, team := range teams {
88+
for _, allowTeam := range api.GithubOAuth2Config.AllowTeams {
89+
if allowTeam.Organization != *selectedMembership.Organization.Login {
90+
// This needs to continue because multiple organizations
91+
// could exist in the allow/team listings.
92+
continue
93+
}
94+
if allowTeam.Slug != *team.Slug {
95+
continue
96+
}
97+
allowedTeam = team
98+
break
99+
}
100+
}
101+
102+
if allowedTeam == nil {
103+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
104+
Message: fmt.Sprintf("You aren't a member of an authorized team in the %s Github organization!", *selectedMembership.Organization.Login),
105+
})
106+
return
107+
}
108+
}
109+
67110
emails, err := api.GithubOAuth2Config.ListEmails(r.Context(), oauthClient)
68111
if err != nil {
69112
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{

coderd/userauth_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,30 @@ func TestUserOAuth2Github(t *testing.T) {
7373
resp := oauth2Callback(t, client)
7474
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
7575
})
76+
t.Run("NotInAllowedTeam", func(t *testing.T) {
77+
t.Parallel()
78+
client := coderdtest.New(t, &coderdtest.Options{
79+
GithubOAuth2Config: &coderd.GithubOAuth2Config{
80+
AllowOrganizations: []string{"coder"},
81+
AllowTeams: []coderd.GithubOAuth2Team{{"another", "something"}, {"coder", "frontend"}},
82+
OAuth2Config: &oauth2Config{},
83+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
84+
return []*github.Membership{{
85+
Organization: &github.Organization{
86+
Login: github.String("coder"),
87+
},
88+
}}, nil
89+
},
90+
ListTeams: func(ctx context.Context, client *http.Client, org string) ([]*github.Team, error) {
91+
return []*github.Team{{
92+
Slug: github.String("nope"),
93+
}}, nil
94+
},
95+
},
96+
})
97+
resp := oauth2Callback(t, client)
98+
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
99+
})
76100
t.Run("UnverifiedEmail", func(t *testing.T) {
77101
t.Parallel()
78102
client := coderdtest.New(t, &coderdtest.Options{
@@ -184,6 +208,43 @@ func TestUserOAuth2Github(t *testing.T) {
184208
resp := oauth2Callback(t, client)
185209
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
186210
})
211+
t.Run("SignupAllowedTeam", func(t *testing.T) {
212+
t.Parallel()
213+
client := coderdtest.New(t, &coderdtest.Options{
214+
GithubOAuth2Config: &coderd.GithubOAuth2Config{
215+
AllowSignups: true,
216+
AllowOrganizations: []string{"coder"},
217+
AllowTeams: []coderd.GithubOAuth2Team{{"coder", "frontend"}},
218+
OAuth2Config: &oauth2Config{},
219+
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
220+
return []*github.Membership{{
221+
Organization: &github.Organization{
222+
Login: github.String("coder"),
223+
},
224+
}}, nil
225+
},
226+
ListTeams: func(ctx context.Context, client *http.Client, org string) ([]*github.Team, error) {
227+
return []*github.Team{{
228+
Slug: github.String("frontend"),
229+
}}, nil
230+
},
231+
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
232+
return &github.User{
233+
Login: github.String("kyle"),
234+
}, nil
235+
},
236+
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
237+
return []*github.UserEmail{{
238+
Email: github.String("[email protected]"),
239+
Verified: github.Bool(true),
240+
Primary: github.Bool(true),
241+
}}, nil
242+
},
243+
},
244+
})
245+
resp := oauth2Callback(t, client)
246+
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
247+
})
187248
}
188249

189250
func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response {

0 commit comments

Comments
 (0)