git.iko.soy/typeable/oauth2-proxy
on master help sign in
feat: support for multiple github orgs (#3072)

* fix for github teams * Update github.go * added errorhandling * Update github.md * refactored GitHub provider refactored hasOrg, hasOrgAndTeams and hasTeam into hasAccess to stay within function limit * reverted Refactoring * refactored github.go - joined hasOrgAndTeamAccess into checkRestrictions * refactored github.go - reduced number of returns of function checkRestrictions to 4 * updated GitHub provider to accept legacy team ids * GoFmt and golangci-lint Formatted with GoFmt and followed recommendations of GoLint * added Tests added Tests for checkRestrictions. * refactored in maintainer feedback * Removed code, documentation and tests for legacy ids * add changelog and update docs --------- Signed-off-by: Jan Larwig <jan@larwig.com> Co-authored-by: Jan Larwig <jan@larwig.com>

commit7731437af49c2767dac211694b2eec70f71b799c authorDaniel Mersch <94058351+daniel-mersch@users.noreply.github.com> dateThu 29 May 2025, 18:11 (1 year ago) parentfb7e335
4 files changed+201−26
CHANGELOG.md+2 docs/docs/configuration/providers/github.md+24−10 providers/github.go+46−16 providers/github_test.go+129
CHANGELOG.md+2
@@ -8,6 +8,8 @@
88
99## Changes since v7.9.0
1010
11+- [#3072](https://github.com/oauth2-proxy/oauth2-proxy/pull/3072) feat: support for multiple github orgs #3072 (@daniel-mersch)
12+
1113# V7.9.0
1214
1315## Release Highlights
docs/docs/configuration/providers/github.md+24−10
@@ -8,7 +8,7 @@ title: GitHub
88| Flag | Toml Field | Type | Description | Default |
99| ---------------- | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------- | ------- |
1010| `--github-org` | `github_org` | string | restrict logins to members of this organisation | |
11−| `--github-team` | `github_team` | string | restrict logins to members of any of these teams (slug), separated by a comma | |
11+| `--github-team` | `github_team` | string | restrict logins to members of any of these teams (slug) or (org:team), comma separated | |
1212| `--github-repo` | `github_repo` | string | restrict logins to collaborators of this repository formatted as `orgname/repo` | |
1313| `--github-token` | `github_token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | |
1414| `--github-user` | `github_users` | string \| list | To allow users to login by username even if they do not belong to the specified org and team or collaborators | |
@@ -24,23 +24,36 @@ team level access, or to collaborators of a repository. Restricting by these opt
2424NOTE: When `--github-user` is set, the specified users are allowed to log in even if they do not belong to the specified
2525org and team or collaborators.
2626
27−To restrict by organization only, include the following flag:
27+To restrict access to your organization:
2828
2929```shell
30− --github-org="" # restrict logins to members of this organisation
30+ # restrict logins to members of this organisation
31+ --github-org="your-org"
3132```
3233
33−To restrict within an organization to specific teams, include the following flag in addition to `-github-org`:
34+To restrict access to specific teams within an organization:
3435
3536```shell
36− --github-team="" # restrict logins to members of any of these teams (slug), separated by a comma
37+ --github-org="your-org"
38+ # restrict logins to members of any of these teams (slug), comma separated
39+ --github-team="team1,team2,team3"
40+```
41+
42+To restrict to teams within different organizations, keep the organization flag empty and use `--github-team` like so:
43+
44+```shell
45+ # keep empty
46+ --github-org=""
47+ # restrict logins to members to any of the following teams (format <org>:<slug>, like octo:team1), comma separated
48+ --github-team="org1:team1,org2:team1,org3:team42,octo:cat"
3749```
3850
3951If you would rather restrict access to collaborators of a repository, those users must either have push access to a
4052public repository or any access to a private repository:
4153
4254```shell
43− --github-repo="" # restrict logins to collaborators of this repository formatted as orgname/repo
55+ # restrict logins to collaborators of this repository formatted as orgname/repo
56+ --github-repo=""
4457```
4558
4659If you'd like to allow access to users with **read only** access to a **public** repository you will need to provide a
@@ -48,14 +61,15 @@ If you'd like to allow access to users with **read only** access to a **public**
4861created with at least the `public_repo` scope:
4962
5063```shell
51− --github-token="" # the token to use when verifying repository collaborators
64+ # the token to use when verifying repository collaborators
65+ --github-token=""
5266```
5367
54−To allow a user to log in with their username even if they do not belong to the specified org and team or collaborators,
55−separated by a comma
68+To allow a user to log in with their username even if they do not belong to the specified org and team or collaborators:
5669
5770```shell
58− --github-user="" #allow logins by username, separated by a comma
71+ # allow logins by username, comma separated
72+ --github-user=""
5973```
6074
6175If you are using GitHub enterprise, make sure you set the following to the appropriate url:
providers/github.go+46−16
@@ -200,6 +200,7 @@ func (p *GitHubProvider) hasOrgAndTeam(s *sessions.SessionState) error {
200200
201201 if strings.EqualFold(p.Org, ot.Org) {
202202 hasOrg = true
203+
203204 teams := strings.Split(p.Team, ",")
204205 for _, team := range teams {
205206 if strings.EqualFold(strings.TrimSpace(team), ot.Team) {
@@ -220,6 +221,37 @@ func (p *GitHubProvider) hasOrgAndTeam(s *sessions.SessionState) error {
220221 return errors.New("user is missing required organization")
221222}
222223
224+func (p *GitHubProvider) hasTeam(s *sessions.SessionState) error {
225+ var teams []string
226+
227+ for _, group := range s.Groups {
228+ if strings.Contains(group, orgTeamSeparator) {
229+ teams = append(teams, strings.TrimSpace(group))
230+ }
231+ }
232+
233+ var presentTeams = make([]string, 0, len(teams))
234+
235+ for _, ot := range teams {
236+ allowedTeams := strings.Split(p.Team, ",")
237+ for _, team := range allowedTeams {
238+ if !strings.Contains(team, orgTeamSeparator) {
239+ logger.Printf("Please use fully qualified team names (org:team-slug) if you omit the organisation. Current Team name: %s", team)
240+ return errors.New("team name is invalid")
241+ }
242+
243+ if strings.EqualFold(strings.TrimSpace(team), ot) {
244+ logger.Printf("Found Github Organization/Team:%s", ot)
245+ return nil
246+ }
247+ }
248+ presentTeams = append(presentTeams, ot)
249+ }
250+
251+ logger.Printf("Missing Team:%q in teams: %v", p.Team, presentTeams)
252+ return errors.New("user is missing required team")
253+}
254+
223255func (p *GitHubProvider) hasRepoAccess(ctx context.Context, accessToken string) error {
224256 // https://developer.github.com/v3/repos/#get-a-repository
225257
@@ -378,12 +410,22 @@ func (p *GitHubProvider) checkRestrictions(ctx context.Context, s *sessions.Sess
378410 return err
379411 }
380412
381− if err := p.hasOrgAndTeamAccess(s); err != nil {
413+ var err error
414+ switch {
415+ case p.Org != "" && p.Team != "":
416+ err = p.hasOrgAndTeam(s)
417+ case p.Org != "":
418+ err = p.hasOrg(s)
419+ case p.Team != "":
420+ err = p.hasTeam(s)
421+ }
422+
423+ if err != nil {
382424 return err
383425 }
384426
385427 if p.Org == "" && p.Repo != "" && p.Token == "" {
386− // If we have a token we'll do the collaborator check in GetUserName
428+ // If we have a token we'll do the collaborator check
387429 return p.hasRepoAccess(ctx, s.AccessToken)
388430 }
389431
@@ -408,18 +450,6 @@ func (p *GitHubProvider) checkUserRestriction(ctx context.Context, s *sessions.S
408450 return verifiedUser, nil
409451}
410452
411−func (p *GitHubProvider) hasOrgAndTeamAccess(s *sessions.SessionState) error {
412− if p.Org != "" && p.Team != "" {
413− return p.hasOrgAndTeam(s)
414− }
415−
416− if p.Org != "" {
417− return p.hasOrg(s)
418− }
419−
420− return nil
421−}
422−
423453func (p *GitHubProvider) getOrgAndTeam(ctx context.Context, s *sessions.SessionState) error {
424454 err := p.getOrgs(ctx, s)
425455 if err != nil {
@@ -503,8 +533,8 @@ func (p *GitHubProvider) getTeams(ctx context.Context, s *sessions.SessionState)
503533 }
504534
505535 for _, team := range teams {
506− logger.Printf("Member of Github Organization/Team:%q/%q", team.Org.Login, team.Slug)
507− s.Groups = append(s.Groups, team.Org.Login+orgTeamSeparator+team.Slug)
536+ logger.Printf("Member of Github Organization/Team: %q/%q", team.Org.Login, team.Slug)
537+ s.Groups = append(s.Groups, fmt.Sprintf("%s%s%s", team.Org.Login, orgTeamSeparator, team.Slug))
508538 }
509539
510540 pn++
providers/github_test.go+129
@@ -39,6 +39,7 @@ func testGitHubBackend(payloads map[string][]string) *httptest.Server {
3939 "/repos/oauth2-proxy/oauth2-proxy/collaborators/mbland": {""},
4040 "/user": {""},
4141 "/user/emails": {""},
42+ "/user/teams": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"},
4243 "/user/orgs": {"page=1&per_page=100", "page=2&per_page=100", "page=3&per_page=100"},
4344 // GitHub Enterprise Server API
4445 "/api/v3": {""},
@@ -168,6 +169,134 @@ func TestGitHubProvider_getEmailWithOrg(t *testing.T) {
168169 assert.Equal(t, "michael.bland@gsa.gov", session.Email)
169170}
170171
172+func TestGitHubProvider_checkRestrictionsOrg(t *testing.T) {
173+ b := testGitHubBackend(map[string][]string{
174+ "/user/emails": {`[ {"email": "john.doe@example", "verified": true, "primary": true} ]`},
175+ "/user/orgs": {
176+ `[ { "login": "test-org-1" } ]`,
177+ `[ { "login": "test-org-2" } ]`,
178+ `[ ]`,
179+ },
180+ "/user/teams": {
181+ `[ { "name":"test-team-1", "slug":"test-team-1", "organization": { "login": "test-org-1" } } ]`,
182+ `[ { "name":"test-team-2", "slug":"test-team-2", "organization": { "login": "test-org-2" } } ]`,
183+ `[ ]`,
184+ },
185+ })
186+ defer b.Close()
187+
188+ // This test should succeed, because of the valid organization.
189+ bURL, _ := url.Parse(b.URL)
190+ p := testGitHubProvider(bURL.Host,
191+ options.GitHubOptions{
192+ Org: "test-org-1",
193+ },
194+ )
195+
196+ var err error
197+ session := CreateAuthorizedSession()
198+ err = p.getOrgAndTeam(context.Background(), session)
199+ assert.NoError(t, err)
200+ err = p.checkRestrictions(context.Background(), session)
201+ assert.NoError(t, err)
202+
203+ // This part should fail, because user is not part of the organization
204+ p = testGitHubProvider(bURL.Host,
205+ options.GitHubOptions{
206+ Org: "test-org-1-fail",
207+ },
208+ )
209+
210+ err = p.checkRestrictions(context.Background(), session)
211+ assert.Error(t, err)
212+}
213+
214+func TestGitHubProvider_checkRestrictionsOrgTeam(t *testing.T) {
215+ b := testGitHubBackend(map[string][]string{
216+ "/user/emails": {`[ {"email": "john.doe@example", "verified": true, "primary": true} ]`},
217+ "/user/orgs": {
218+ `[ { "login": "test-org-1" } ]`,
219+ `[ { "login": "test-org-2" } ]`,
220+ `[ ]`,
221+ },
222+ "/user/teams": {
223+ `[ { "name":"test-team-1", "slug":"test-team-1", "organization": { "login": "test-org-1" } } ]`,
224+ `[ { "name":"test-team-2", "slug":"test-team-2", "organization": { "login": "test-org-2" } } ]`,
225+ `[ ]`,
226+ },
227+ })
228+ defer b.Close()
229+
230+ // This test should succeed, because of the valid Org and Team combination.
231+ bURL, _ := url.Parse(b.URL)
232+ p := testGitHubProvider(bURL.Host,
233+ options.GitHubOptions{
234+ Org: "test-org-1",
235+ Team: "test-team-1",
236+ },
237+ )
238+
239+ var err error
240+ session := CreateAuthorizedSession()
241+ err = p.getOrgAndTeam(context.Background(), session)
242+ assert.NoError(t, err)
243+ err = p.checkRestrictions(context.Background(), session)
244+ assert.NoError(t, err)
245+
246+ // This part should fail, because user is not part of the organization and the team
247+ p = testGitHubProvider(bURL.Host,
248+ options.GitHubOptions{
249+ Org: "test-org-1-fail",
250+ Team: "test-team-1-fail",
251+ },
252+ )
253+
254+ err = p.checkRestrictions(context.Background(), session)
255+ assert.Error(t, err)
256+}
257+
258+func TestGitHubProvider_checkRestrictionsTeam(t *testing.T) {
259+ b := testGitHubBackend(map[string][]string{
260+ "/user/emails": {`[ {"email": "john.doe@example", "verified": true, "primary": true} ]`},
261+ "/user/orgs": {
262+ `[ { "login": "test-org-1" } ]`,
263+ `[ { "login": "test-org-2" } ]`,
264+ `[ ]`,
265+ },
266+ "/user/teams": {
267+ `[ { "name":"test-team-1", "slug":"test-team-1", "organization": { "login": "test-org-1" } } ]`,
268+ `[ { "name":"test-team-2", "slug":"test-team-2", "organization": { "login": "test-org-2" } } ]`,
269+ `[ ]`,
270+ },
271+ })
272+ defer b.Close()
273+
274+ // This test should succeed, because of the valid org:slug combination.
275+ bURL, _ := url.Parse(b.URL)
276+ p := testGitHubProvider(bURL.Host,
277+ options.GitHubOptions{
278+ Team: "test-org-1:test-team-1, test-org-2:test-team-2-fail",
279+ },
280+ )
281+
282+ var err error
283+ session := CreateAuthorizedSession()
284+ err = p.getOrgAndTeam(context.Background(), session)
285+ assert.NoError(t, err)
286+ err = p.checkRestrictions(context.Background(), session)
287+ assert.NoError(t, err)
288+
289+ // This part should fail, because user is not part of the organization:team combination
290+ p = testGitHubProvider(bURL.Host,
291+ options.GitHubOptions{
292+ Team: "test-org-1-fail:test-team-1-fail, test-org-2-fail:test-team-2-fail",
293+ },
294+ )
295+
296+ err = p.checkRestrictions(context.Background(), session)
297+ assert.Error(t, err)
298+}
299+
171300func TestGitHubProvider_getEmailWithWriteAccessToPublicRepo(t *testing.T) {
172301 b := testGitHubBackend(map[string][]string{
173302 "/repo/oauth2-proxy/oauth2-proxy": {`{"permissions": {"pull": true, "push": true}, "private": false}`},