Skip to content

Commit 7731437

Browse files
feat: support for multiple github orgs (oauth2-proxy#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 <[email protected]> Co-authored-by: Jan Larwig <[email protected]>
1 parent fb7e335 commit 7731437

File tree

4 files changed

+201
-26
lines changed

4 files changed

+201
-26
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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,38 +24,52 @@ team level access, or to collaborators of a repository. Restricting by these opt
2424
NOTE: When `--github-user` is set, the specified users are allowed to log in even if they do not belong to the specified
2525
org 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

3951
If you would rather restrict access to collaborators of a repository, those users must either have push access to a
4052
public 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

4659
If you'd like to allow access to users with **read only** access to a **public** repository you will need to provide a
4760
[token](https://github.com/settings/tokens) for a user that has write access to the repository. The token must be
4861
created 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

6175
If you are using GitHub enterprise, make sure you set the following to the appropriate url:

providers/github.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
223255
func (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-
423453
func (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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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, "[email protected]", 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+
171300
func TestGitHubProvider_getEmailWithWriteAccessToPublicRepo(t *testing.T) {
172301
b := testGitHubBackend(map[string][]string{
173302
"/repo/oauth2-proxy/oauth2-proxy": {`{"permissions": {"pull": true, "push": true}, "private": false}`},

0 commit comments

Comments
 (0)