git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add authorization support for Gitlab projects (#630)

* Add support for gitlab projets * Add group membership in state * Use prefixed allowed groups everywhere * Fix: remove unused function * Fix: rename func that add data to session * Simplify projects and groups session funcs * Add project access level for gitlab projects * Fix: default access level * Add per project access level * Add user email when missing access level * Fix: harmonize errors * Update docs and flags description for gitlab project * Add test with both projects and groups * Fix: log error message Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Fix: make doc a markdown link * Add notes about read_api scope for projects * Fix: Verifier override in Gitlab Provider This commit fixes a bug caused by an override of the Verifier value from *ProviderData inside GitlabProvider struct * Fix: ensure data in session before using it * Update providers/gitlab.go Co-authored-by: Nick Meves <nick.meves@greenhouse.io> * Rename gitlab project initializer * Improve return value readbility * Use splitN * Handle space delimiters in set project scope * Reword comment for AddProjects * Fix: typo * Rework error handling in addProjectsToSession * Reduce branching complexity in addProjectsToSession * Fix: line returns * Better comment for addProjectsToSession * Fix: enrich session comment * Fix: email domains is handled before provider mechanism * Add archived project unit test * Fix: emails handling in gitlab provider Co-authored-by: Wilfried OLLIVIER <wollivier@bearstech.com> Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> Co-authored-by: Nick Meves <nick.meves@greenhouse.io>

commitd67d6e3152e2a1c9614906ee9bd9890f8da9a007 authorMathieu Lecarme <mathieu@garambrogne.net> dateSat 5 Dec 2020, 19:57 (6 years ago) parent5117f23
8 files changed+437−111
CHANGELOG.md+35−21 docs/docs/configuration/auth.md+2 docs/docs/configuration/overview.md+1 pkg/apis/options/options.go+2 pkg/validation/options.go+6 providers/gitlab.go+165−23 providers/gitlab_test.go+210−67 providers/providers_suite_test.go+16
CHANGELOG.md+35−21
@@ -4,13 +4,14 @@
44
55## Important Notes
66
7+- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled
78- [#905](https://github.com/oauth2-proxy/oauth2-proxy/pull/905) Existing sessions from v6.0.0 or earlier are no longer valid. They will trigger a reauthentication.
89- [#826](https://github.com/oauth2-proxy/oauth2-proxy/pull/826) `skip-auth-strip-headers` now applies to all requests, not just those where authentication would be skipped.
910- [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) The behavior of the Google provider Groups restriction changes with this
1011 - Either `--google-group` or the new `--allowed-group` will work for Google now (`--google-group` will be used if both are set)
1112 - Group membership lists will be passed to the backend with the `X-Forwarded-Groups` header
1213 - If you change the list of allowed groups, existing sessions that now don't have a valid group will be logged out immediately.
13− - Previously, group membership was only checked on session creation and refresh.
14+ - Previously, group membership was only checked on session creation and refresh.
1415- [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) `--skip-auth-route` is (almost) backwards compatible with `--skip-auth-regex`
1516 - We are marking `--skip-auth-regex` as DEPRECATED and will remove it in the next major version.
1617 - If your regex contains an `=` and you want it for all methods, you will need to add a leading `=` (this is the area where `--skip-auth-regex` doesn't port perfectly)
@@ -38,11 +39,12 @@
3839 be any redirects in the browser anymore when tokens expire, but instead a token refresh is initiated
3940 in the background, which leads to new tokens being returned in the cookies.
4041 - Please note that `--cookie-refresh` must be 0 (the default) or equal to the token lifespan configured in Azure AD to make
41− Azure token refresh reliable. Setting this value to 0 means that it relies on the provider implementation
42− to decide if a refresh is required.
42+ Azure token refresh reliable. Setting this value to 0 means that it relies on the provider implementation
43+ to decide if a refresh is required.
4344
4445## Changes since v6.1.1
4546
47+- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Add support for Gitlab project based authentication (@factorysh)
4648- [#907](https://github.com/oauth2-proxy/oauth2-proxy/pull/907) Introduce alpha configuration option to enable testing of structured configuration (@JoelSpeed)
4749- [#938](https://github.com/oauth2-proxy/oauth2-proxy/pull/938) Cleanup missed provider renaming refactor methods (@NickMeves)
4850- [#925](https://github.com/oauth2-proxy/oauth2-proxy/pull/925) Fix basic auth legacy header conversion (@JoelSpeed)
@@ -78,7 +80,6 @@
7880- [#829](https://github.com/oauth2-proxy/oauth2-proxy/pull/820) Rename test directory to testdata (@johejo)
7981- [#819](https://github.com/oauth2-proxy/oauth2-proxy/pull/819) Improve CI (@johejo)
8082
81−
8283# v6.1.1
8384
8485## Release Highlights
@@ -180,7 +181,7 @@ N/A
180181- [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API
181182 - The Azure AD Graph API has been [deprecated](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api) and is being replaced by the Microsoft Graph API.
182183 If your application relies on the access token being passed to it to access the Azure AD Graph API, you should migrate your application to use the Microsoft Graph API.
183− Existing behaviour can be retained by setting `-resource=https://graph.windows.net`.
184+ Existing behaviour can be retained by setting `-resource=https://graph.windows.net`.
184185- [#484](https://github.com/oauth2-proxy/oauth2-proxy/pull/484) Configuration loading has been replaced with Viper and PFlag
185186 - Flags now require a `--` prefix before the option
186187 - Previously flags allowed either `-` or `--` to prefix the option name
@@ -201,7 +202,7 @@ N/A
201202- [#556](https://github.com/oauth2-proxy/oauth2-proxy/pull/556) Remove unintentional auto-padding of secrets that were too short
202203 - Previously, after cookie-secrets were opportunistically base64 decoded to raw bytes,
203204 they were padded to have a length divisible by 4.
204− - This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors
205+ - This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors
205206 reporting an invalid length of 20 or 28 when the user input cookie-secret was not that length.
206207 - Now we will only base64 decode a cookie-secret to raw bytes if it is 16, 24, or 32 bytes long. Otherwise, we will convert
207208 the direct cookie-secret to bytes without silent padding added.
@@ -306,15 +307,18 @@ N/A
306307# v5.1.0
307308
308309## Release Highlights
310+
309311- Bump to Go 1.14
310312- Reduced number of Google API requests for group validation
311313- Support for Redis Cluster
312314- Support for overriding hosts in hosts file
313315
314316## Important Notes
317+
315318- [#335] The session expiry for the OIDC provider is now taken from the Token Response (expires_in) rather than from the id_token (exp)
316319
317320## Breaking Changes
321+
318322N/A
319323
320324## Changes since v5.0.0
@@ -338,13 +342,15 @@ N/A
338342# v5.0.0
339343
340344## Release Highlights
345+
341346- Disabled CGO (binaries will work regardless og glibc/musl)
342347- Allow whitelisted redirect ports
343348- Nextcloud provider support added
344349- DigitalOcean provider support added
345350
346351## Important Notes
347−- (Security) Fix for [open redirect vulnerability](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-qqxw-m5fj-f7gv).. a bad actor using `/\` in redirect URIs can redirect a session to another domain
352+
353+- (Security) Fix for [open redirect vulnerability](https://github.com/oauth2-proxy/oauth2-proxy/security/advisories/GHSA-qqxw-m5fj-f7gv).. a bad actor using `/\` in redirect URIs can redirect a session to another domain
348354
349355## Breaking Changes
350356
@@ -365,6 +371,7 @@ N/A
365371# v4.1.0
366372
367373## Release Highlights
374+
368375- Added Keycloak provider
369376- Build on Go 1.13
370377- Upgrade Docker image to use Debian Buster
@@ -373,12 +380,15 @@ N/A
373380- Added support for GitHub teams
374381
375382## Important Notes
383+
376384N/A
377385
378386## Breaking Changes
387+
379388N/A
380389
381390## Changes since v4.0.0
391+
382392- [#292](https://github.com/oauth2-proxy/oauth2-proxy/pull/292) Added bash >= 4.0 dependency to configure script (@jmfrank63)
383393- [#227](https://github.com/oauth2-proxy/oauth2-proxy/pull/227) Add Keycloak provider (@Ofinka)
384394- [#259](https://github.com/oauth2-proxy/oauth2-proxy/pull/259) Redirect to HTTPS (@jmickey)
@@ -401,6 +411,7 @@ N/A
401411# v4.0.0
402412
403413## Release Highlights
414+
404415- Documentation is now on a [microsite](https://oauth2-proxy.github.io/oauth2-proxy/)
405416- Health check logging can now be disabled for quieter logs
406417- Authorization Header JWTs can now be verified by the proxy to skip authentication for machine users
@@ -408,29 +419,30 @@ N/A
408419- Logging overhaul allows customisable logging formats
409420
410421## Important Notes
422+
411423- This release includes a number of breaking changes that will require users to
412−reconfigure their proxies. Please read the Breaking Changes below thoroughly.
424+ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
413425
414426## Breaking Changes
415427
416428- [#231](https://github.com/oauth2-proxy/oauth2-proxy/pull/231) Rework GitLab provider
417429 - This PR changes the configuration options for the GitLab provider to use
418− a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than
419− explicit `-login-url`, `-redeem-url` and `-validate-url` parameters.
430+ a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than
431+ explicit `-login-url`, `-redeem-url` and `-validate-url` parameters.
420432- [#186](https://github.com/oauth2-proxy/oauth2-proxy/pull/186) Make config consistent
421433 - This PR changes configuration options so that all flags have a config counterpart
422− of the same name but with underscores (`_`) in place of hyphens (`-`).
423− This change affects the following flags:
434+ of the same name but with underscores (`_`) in place of hyphens (`-`).
435+ This change affects the following flags:
424436 - The `--tls-key` flag is now `--tls-key-file` to be consistent with existing
425− file flags and the existing config and environment settings
437+ file flags and the existing config and environment settings
426438 - The `--tls-cert` flag is now `--tls-cert-file` to be consistent with existing
427− file flags and the existing config and environment settings
428− This change affects the following existing configuration options:
439+ file flags and the existing config and environment settings
440+ This change affects the following existing configuration options:
429441 - The `proxy-prefix` option is now `proxy_prefix`.
430− This PR changes environment variables so that all flags have an environment
431− counterpart of the same name but capitalised, with underscores (`_`) in place
432− of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`.
433− This change affects the following existing environment variables:
442+ This PR changes environment variables so that all flags have an environment
443+ counterpart of the same name but capitalised, with underscores (`_`) in place
444+ of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`.
445+ This change affects the following existing environment variables:
434446 - The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`.
435447 - The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`.
436448- [#146](https://github.com/oauth2-proxy/oauth2-proxy/pull/146) Use full email address as `User` if the auth response did not contain a `User` field
@@ -456,7 +468,7 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
456468- [#65](https://github.com/oauth2-proxy/oauth2-proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via
457469 the `-skip-jwt-bearer-token` options. (@brianv0)
458470 - Additional verifiers can be configured via the `-extra-jwt-issuers` flag if the JWT issuers is either an OpenID provider or has a JWKS URL
459− (e.g. `https://example.com/.well-known/jwks.json`).
471+ (e.g. `https://example.com/.well-known/jwks.json`).
460472- [#180](https://github.com/oauth2-proxy/oauth2-proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg).
461473- [#175](https://github.com/oauth2-proxy/oauth2-proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg).
462474 - Includes fix for potential signature checking issue when OIDC discovery is skipped.
@@ -514,6 +526,7 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
514526# v3.2.0
515527
516528## Release highlights
529+
517530- Internal restructure of session state storage to use JSON rather than proprietary scheme
518531- Added health check options for running on GCP behind a load balancer
519532- Improved support for protecting websockets
@@ -521,9 +534,10 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly.
521534- Allow manual configuration of OIDC providers
522535
523536## Important notes
537+
524538- Dockerfile user is now non-root, this may break your existing deployment
525539- In the OIDC provider, when no email is returned, the ID Token subject will be used
526−instead of returning an error
540+ instead of returning an error
527541- GitHub user emails must now be primary and verified before authenticating
528542
529543## Changes since v3.1.0
docs/docs/configuration/auth.md+2
@@ -149,6 +149,8 @@ The group management in keycloak is using a tree. If you create a group named ad
149149
150150Whether you are using GitLab.com or self-hosting GitLab, follow [these steps to add an application](https://docs.gitlab.com/ce/integration/oauth_provider.html). Make sure to enable at least the `openid`, `profile` and `email` scopes, and set the redirect url to your application url e.g. https://myapp.com/oauth2/callback.
151151
152+If you need projects filtering, add the extra `read_api` scope to your application.
153+
152154The following config should be set to ensure that the oauth will work properly. To get a cookie secret follow [these steps](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/docs/configuration/configuration.md#configuration)
153155
154156```
docs/docs/configuration/overview.md+1
@@ -54,6 +54,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
5454| `--github-token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | |
5555| `--github-user` | string \| list | To allow users to login by username even if they do not belong to the specified org and team or collaborators | |
5656| `--gitlab-group` | string \| list | restrict logins to members of any of these groups (slug), separated by a comma | |
57+| `--gitlab-projects` | string \| list | restrict logins to members of any of these projects (may be given multiple times) formatted as `orgname/repo=accesslevel`. Access level should be a value matching [Gitlab access levels](https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent | |
5758| `--google-admin-email` | string | the google admin to impersonate for api calls | |
5859| `--google-group` | string | restrict logins to members of this google group (may be given multiple times). | |
5960| `--google-service-account-json` | string | the path to the service account json credentials | |
pkg/apis/options/options.go+2
@@ -48,6 +48,7 @@ type Options struct {
4848 GitHubToken string `flag:"github-token" cfg:"github_token"`
4949 GitHubUsers []string `flag:"github-user" cfg:"github_users"`
5050 GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"`
51+ GitlabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"`
5152 GoogleGroups []string `flag:"google-group" cfg:"google_group"`
5253 GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"`
5354 GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"`
@@ -188,6 +189,7 @@ func NewFlagSet() *pflag.FlagSet {
188189 flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)")
189190 flagSet.StringSlice("github-user", []string{}, "allow users with these usernames to login even if they do not belong to the specified org and team or collaborators (may be given multiple times)")
190191 flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)")
192+ flagSet.StringSlice("gitlab-project", []string{}, "restrict logins to members of this project (may be given multiple times) (eg `group/project=accesslevel`). Access level should be a value matching Gitlab access levels (see https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent")
191193 flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).")
192194 flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls")
193195 flagSet.String("google-service-account-json", "", "the path to the service account json credentials")
pkg/validation/options.go+6
@@ -282,6 +282,12 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
282282 case *providers.GitLabProvider:
283283 p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
284284 p.Groups = o.GitLabGroup
285+ err := p.AddProjects(o.GitlabProjects)
286+ if err != nil {
287+ msgs = append(msgs, "failed to setup gitlab project access level")
288+ }
289+ p.SetAllowedGroups(p.PrefixAllowedGroups())
290+ p.SetProjectScope()
285291
286292 if p.Verifier == nil {
287293 // Initialize with default verifier for gitlab.com
providers/gitlab.go+165−23
@@ -3,10 +3,13 @@ package providers
33import (
44 "context"
55 "fmt"
6+ "net/url"
7+ "strconv"
8+ "strings"
69 "time"
710
8− oidc "github.com/coreos/go-oidc"
911 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
12+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1013 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
1114 "golang.org/x/oauth2"
1215)
@@ -15,11 +18,54 @@ import (
1518type GitLabProvider struct {
1619 *ProviderData
1720
18− Groups []string
19− Verifier *oidc.IDTokenVerifier
21+ Groups []string
22+ Projects []*GitlabProject
23+
2024 AllowUnverifiedEmail bool
2125}
2226
27+// GitlabProject represents a Gitlab project constraint entity
28+type GitlabProject struct {
29+ Name string
30+ AccessLevel int
31+}
32+
33+// newGitlabProject Creates a new GitlabProject struct from project string formatted as namespace/project=accesslevel
34+// if no accesslevel provided, use the default one
35+func newGitlabproject(project string) (*GitlabProject, error) {
36+ // default access level is 20
37+ defaultAccessLevel := 20
38+ // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels
39+ validAccessLevel := [4]int{10, 20, 30, 40}
40+
41+ parts := strings.SplitN(project, "=", 2)
42+
43+ if len(parts) == 2 {
44+ lvl, err := strconv.Atoi(parts[1])
45+ if err != nil {
46+ return nil, err
47+ }
48+
49+ for _, valid := range validAccessLevel {
50+ if lvl == valid {
51+ return &GitlabProject{
52+ Name: parts[0],
53+ AccessLevel: lvl},
54+ err
55+ }
56+ }
57+
58+ return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0])
59+
60+ }
61+
62+ return &GitlabProject{
63+ Name: project,
64+ AccessLevel: defaultAccessLevel},
65+ nil
66+
67+}
68+
2369var _ Provider = (*GitLabProvider)(nil)
2470
2571const (
@@ -64,6 +110,19 @@ func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) (
64110 return
65111}
66112
113+// SetProjectScope ensure read_api is added to scope when filtering on projects
114+func (p *GitLabProvider) SetProjectScope() {
115+ if len(p.Projects) > 0 {
116+ for _, val := range strings.Split(p.Scope, " ") {
117+ if val == "read_api" {
118+ return
119+ }
120+
121+ }
122+ p.Scope += " read_api"
123+ }
124+}
125+
67126// RefreshSessionIfNeeded checks if the session has expired and uses the
68127// RefreshToken to fetch a new ID token if required
69128func (p *GitLabProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
@@ -144,25 +203,56 @@ func (p *GitLabProvider) getUserInfo(ctx context.Context, s *sessions.SessionSta
144203 return &userInfo, nil
145204}
146205
147−func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error {
148− if len(p.Groups) == 0 {
149− return nil
206+type gitlabPermissionAccess struct {
207+ AccessLevel int `json:"access_level"`
208+}
209+
210+type gitlabProjectPermission struct {
211+ ProjectAccess *gitlabPermissionAccess `json:"project_access"`
212+ GroupAccess *gitlabPermissionAccess `json:"group_access"`
213+}
214+
215+type gitlabProjectInfo struct {
216+ Name string `json:"name"`
217+ Archived bool `json:"archived"`
218+ PathWithNamespace string `json:"path_with_namespace"`
219+ Permissions gitlabProjectPermission `json:"permissions"`
220+}
221+
222+func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.SessionState, project string) (*gitlabProjectInfo, error) {
223+ var projectInfo gitlabProjectInfo
224+
225+ endpointURL := &url.URL{
226+ Scheme: p.LoginURL.Scheme,
227+ Host: p.LoginURL.Host,
228+ Path: "/api/v4/projects/",
150229 }
151230
152− // Collect user group memberships
153− membershipSet := make(map[string]bool)
154− for _, group := range userInfo.Groups {
155− membershipSet[group] = true
231+ err := requests.New(fmt.Sprintf("%s%s", endpointURL.String(), url.QueryEscape(project))).
232+ WithContext(ctx).
233+ SetHeader("Authorization", "Bearer "+s.AccessToken).
234+ Do().
235+ UnmarshalInto(&projectInfo)
236+
237+ if err != nil {
238+ return nil, fmt.Errorf("failed to get project info: %v", err)
156239 }
157240
158− // Find a valid group that they are a member of
159− for _, validGroup := range p.Groups {
160− if _, ok := membershipSet[validGroup]; ok {
161− return nil
241+ return &projectInfo, nil
242+}
243+
244+// AddProjects adds Gitlab projects from options to GitlabProvider struct
245+func (p *GitLabProvider) AddProjects(projects []string) error {
246+ for _, project := range projects {
247+ gp, err := newGitlabproject(project)
248+ if err != nil {
249+ return err
162250 }
251+
252+ p.Projects = append(p.Projects, gp)
163253 }
164254
165− return fmt.Errorf("user is not a member of '%s'", p.Groups)
255+ return nil
166256}
167257
168258func (p *GitLabProvider) createSessionState(ctx context.Context, token *oauth2.Token) (*sessions.SessionState, error) {
@@ -193,7 +283,7 @@ func (p *GitLabProvider) ValidateSession(ctx context.Context, s *sessions.Sessio
193283 return err == nil
194284}
195285
196−// GetEmailAddress returns the Account email address
286+// EnrichSession adds values and data from the Gitlab endpoint to current session
197287func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
198288 // Retrieve user info
199289 userInfo, err := p.getUserInfo(ctx, s)
@@ -206,15 +296,67 @@ func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionS
206296 return fmt.Errorf("user email is not verified")
207297 }
208298
209− // Check group membership
210− // TODO (@NickMeves) - Refactor to Authorize
211− err = p.verifyGroupMembership(userInfo)
212− if err != nil {
213− return fmt.Errorf("group membership check failed: %v", err)
214− }
215−
216299 s.User = userInfo.Username
217300 s.Email = userInfo.Email
218301
302+ p.addGroupsToSession(ctx, s)
303+
304+ p.addProjectsToSession(ctx, s)
305+
219306 return nil
307+
308+}
309+
310+// addGroupsToSession projects into session.Groups
311+func (p *GitLabProvider) addGroupsToSession(ctx context.Context, s *sessions.SessionState) {
312+ // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
313+ for _, group := range p.Groups {
314+ s.Groups = append(s.Groups, fmt.Sprintf("group:%s", group))
315+ }
316+}
317+
318+// addProjectsToSession adds projects matching user access requirements into the session state groups list
319+// This method prefix projects names with `project` to specify group kind
320+func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) {
321+ // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user
322+ for _, project := range p.Projects {
323+ projectInfo, err := p.getProjectInfo(ctx, s, project.Name)
324+
325+ if err != nil {
326+ logger.Errorf("Warning: project info request failed: %v", err)
327+ continue
328+ }
329+
330+ if !projectInfo.Archived {
331+ perms := projectInfo.Permissions.ProjectAccess
332+ if perms == nil {
333+ // use group project access as fallback
334+ perms = projectInfo.Permissions.GroupAccess
335+ }
336+
337+ if perms.AccessLevel >= project.AccessLevel {
338+ s.Groups = append(s.Groups, fmt.Sprintf("project:%s", project.Name))
339+ } else {
340+ logger.Errorf("Warning: user %q does not have the minimum required access level for project %q", s.Email, project.Name)
341+ }
342+ } else {
343+ logger.Errorf("Warning: project %s is archived", project.Name)
344+ }
345+
346+ }
347+
348+}
349+
350+// PrefixAllowedGroups returns a list of allowed groups, prefixed by their `kind` value
351+func (p *GitLabProvider) PrefixAllowedGroups() (groups []string) {
352+
353+ for _, val := range p.Groups {
354+ groups = append(groups, fmt.Sprintf("group:%s", val))
355+ }
356+
357+ for _, val := range p.Projects {
358+ groups = append(groups, fmt.Sprintf("project:%s", val.Name))
359+ }
360+
361+ return groups
220362}
providers/gitlab_test.go+210−67
@@ -2,13 +2,15 @@ package providers
22
33import (
44 "context"
5+ "errors"
56 "net/http"
67 "net/http/httptest"
78 "net/url"
8− "testing"
99
1010 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11− "github.com/stretchr/testify/assert"
11+ . "github.com/onsi/ginkgo"
12+ . "github.com/onsi/ginkgo/extensions/table"
13+ . "github.com/onsi/gomega"
1214)
1315
1416func testGitLabProvider(hostname string) *GitLabProvider {
@@ -39,100 +41,241 @@ func testGitLabBackend() *httptest.Server {
3941 "groups": ["foo", "bar"]
4042 }
4143 `
44+ projectInfo := `
45+ {
46+ "name": "MyProject",
47+ "archived": false,
48+ "path_with_namespace": "my_group/my_project",
49+ "permissions": {
50+ "project_access": null,
51+ "group_access": {
52+ "access_level": 30,
53+ "notification_level": 3
54+ }
55+ }
56+ }
57+ `
58+
59+ personalProjectInfo := `
60+ {
61+ "name": "MyPersonalProject",
62+ "archived": false,
63+ "path_with_namespace": "my_profile/my_personal_project",
64+ "permissions": {
65+ "project_access": {
66+ "access_level": 30,
67+ "notification_level": 3
68+ },
69+ "group_access": null
70+ }
71+ }
72+ `
73+
74+ archivedProjectInfo := `
75+ {
76+ "name": "MyArchivedProject",
77+ "archived": true,
78+ "path_with_namespace": "my_group/my_archived_project",
79+ "permissions": {
80+ "project_access": {
81+ "access_level": 30,
82+ "notification_level": 3
83+ },
84+ "group_access": null
85+ }
86+ }
87+ `
88+
4289 authHeader := "Bearer gitlab_access_token"
4390
4491 return httptest.NewServer(http.HandlerFunc(
4592 func(w http.ResponseWriter, r *http.Request) {
46− if r.URL.Path == "/oauth/userinfo" {
93+ switch r.URL.Path {
94+ case "/oauth/userinfo":
4795 if r.Header["Authorization"][0] == authHeader {
4896 w.WriteHeader(200)
4997 w.Write([]byte(userInfo))
5098 } else {
5199 w.WriteHeader(401)
52100 }
53− } else {
101+ case "/api/v4/projects/my_group/my_project":
102+ if r.Header["Authorization"][0] == authHeader {
103+ w.WriteHeader(200)
104+ w.Write([]byte(projectInfo))
105+ } else {
106+ w.WriteHeader(401)
107+ }
108+ case "/api/v4/projects/my_group/my_archived_project":
109+ if r.Header["Authorization"][0] == authHeader {
110+ w.WriteHeader(200)
111+ w.Write([]byte(archivedProjectInfo))
112+ } else {
113+ w.WriteHeader(401)
114+ }
115+ case "/api/v4/projects/my_profile/my_personal_project":
116+ if r.Header["Authorization"][0] == authHeader {
117+ w.WriteHeader(200)
118+ w.Write([]byte(personalProjectInfo))
119+ } else {
120+ w.WriteHeader(401)
121+ }
122+ case "/api/v4/projects/my_group/my_bad_project":
123+ w.WriteHeader(403)
124+ default:
54125 w.WriteHeader(404)
55126 }
56127 }))
57128}
58129
59−func TestGitLabProviderBadToken(t *testing.T) {
60− b := testGitLabBackend()
61− defer b.Close()
130+var _ = Describe("Gitlab Provider Tests", func() {
131+ var p *GitLabProvider
132+ var b *httptest.Server
62133
63− bURL, _ := url.Parse(b.URL)
64− p := testGitLabProvider(bURL.Host)
134+ BeforeEach(func() {
135+ b = testGitLabBackend()
65136
66− session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
67− err := p.EnrichSession(context.Background(), session)
68− assert.Error(t, err)
69−}
137+ bURL, err := url.Parse(b.URL)
138+ Expect(err).To(BeNil())
70139
71−func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) {
72− b := testGitLabBackend()
73− defer b.Close()
140+ p = testGitLabProvider(bURL.Host)
141+ })
74142
75− bURL, _ := url.Parse(b.URL)
76− p := testGitLabProvider(bURL.Host)
143+ AfterEach(func() {
144+ b.Close()
145+ })
77146
78− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
79− err := p.EnrichSession(context.Background(), session)
80− assert.Error(t, err)
81−}
147+ Context("with bad token", func() {
148+ It("should trigger an error", func() {
149+ p.AllowUnverifiedEmail = false
150+ session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"}
151+ err := p.EnrichSession(context.Background(), session)
152+ Expect(err).To(MatchError(errors.New("failed to retrieve user info: error getting user info: unexpected status \"401\": ")))
153+ })
154+ })
82155
83−func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) {
84− b := testGitLabBackend()
85− defer b.Close()
156+ Context("when filtering on email", func() {
157+ type emailsTableInput struct {
158+ expectedError error
159+ expectedValue string
160+ allowUnverifiedEmail bool
161+ }
86162
87− bURL, _ := url.Parse(b.URL)
88− p := testGitLabProvider(bURL.Host)
89− p.AllowUnverifiedEmail = true
163+ DescribeTable("should return expected results",
164+ func(in emailsTableInput) {
165+ p.AllowUnverifiedEmail = in.allowUnverifiedEmail
166+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
90167
91− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
92− err := p.EnrichSession(context.Background(), session)
93− assert.NoError(t, err)
94− assert.Equal(t, "foo@bar.com", session.Email)
95−}
168+ err := p.EnrichSession(context.Background(), session)
96169
97−func TestGitLabProviderUsername(t *testing.T) {
98− b := testGitLabBackend()
99− defer b.Close()
170+ if in.expectedError != nil {
171+ Expect(err).To(MatchError(err))
172+ } else {
173+ Expect(err).To(BeNil())
174+ Expect(session.Email).To(Equal(in.expectedValue))
175+ }
176+ },
177+ Entry("unverified email denied", emailsTableInput{
178+ expectedError: errors.New("user email is not verified"),
179+ allowUnverifiedEmail: false,
180+ }),
181+ Entry("unverified email allowed", emailsTableInput{
182+ expectedError: nil,
183+ expectedValue: "foo@bar.com",
184+ allowUnverifiedEmail: true,
185+ }),
186+ )
187+ })
100188
101− bURL, _ := url.Parse(b.URL)
102− p := testGitLabProvider(bURL.Host)
103− p.AllowUnverifiedEmail = true
189+ Context("when filtering on gitlab entities (groups and projects)", func() {
190+ type entitiesTableInput struct {
191+ expectedValue []string
192+ projects []string
193+ groups []string
194+ }
104195
105− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
106− err := p.EnrichSession(context.Background(), session)
107− assert.NoError(t, err)
108− assert.Equal(t, "FooBar", session.User)
109−}
196+ DescribeTable("should return expected results",
197+ func(in entitiesTableInput) {
198+ p.AllowUnverifiedEmail = true
199+ session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
110200
111−func TestGitLabProviderGroupMembershipValid(t *testing.T) {
112− b := testGitLabBackend()
113− defer b.Close()
201+ err := p.AddProjects(in.projects)
202+ Expect(err).To(BeNil())
203+ p.SetProjectScope()
114204
115− bURL, _ := url.Parse(b.URL)
116− p := testGitLabProvider(bURL.Host)
117− p.AllowUnverifiedEmail = true
118− p.Groups = []string{"foo"}
205+ if len(in.groups) > 0 {
206+ p.Groups = in.groups
207+ }
119208
120− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
121− err := p.EnrichSession(context.Background(), session)
122− assert.NoError(t, err)
123− assert.Equal(t, "FooBar", session.User)
124−}
209+ err = p.EnrichSession(context.Background(), session)
125210
126−func TestGitLabProviderGroupMembershipMissing(t *testing.T) {
127− b := testGitLabBackend()
128− defer b.Close()
211+ Expect(err).To(BeNil())
212+ Expect(session.Groups).To(Equal(in.expectedValue))
213+ },
214+ Entry("project membership valid on group project", entitiesTableInput{
215+ expectedValue: []string{"project:my_group/my_project"},
216+ projects: []string{"my_group/my_project"},
217+ }),
218+ Entry("project membership invalid on group project, insufficient access level level", entitiesTableInput{
219+ expectedValue: nil,
220+ projects: []string{"my_group/my_project=40"},
221+ }),
222+ Entry("project membership valid on personnal project", entitiesTableInput{
223+ expectedValue: []string{"project:my_profile/my_personal_project"},
224+ projects: []string{"my_profile/my_personal_project"},
225+ }),
226+ Entry("project membership invalid on personnal project, insufficient access level", entitiesTableInput{
227+ expectedValue: nil,
228+ projects: []string{"my_profile/my_personal_project=40"},
229+ }),
230+ Entry("project membership invalid", entitiesTableInput{
231+ expectedValue: nil,
232+ projects: []string{"my_group/my_bad_project"},
233+ }),
234+ Entry("group membership valid", entitiesTableInput{
235+ expectedValue: []string{"group:foo"},
236+ groups: []string{"foo"},
237+ }),
238+ Entry("groups and projects", entitiesTableInput{
239+ expectedValue: []string{"group:foo", "group:baz", "project:my_group/my_project", "project:my_profile/my_personal_project"},
240+ groups: []string{"foo", "baz"},
241+ projects: []string{"my_group/my_project", "my_profile/my_personal_project"},
242+ }),
243+ Entry("archived projects", entitiesTableInput{
244+ expectedValue: nil,
245+ groups: []string{},
246+ projects: []string{"my_group/my_archived_project"},
247+ }),
248+ )
129249
130− bURL, _ := url.Parse(b.URL)
131− p := testGitLabProvider(bURL.Host)
132− p.AllowUnverifiedEmail = true
133− p.Groups = []string{"baz"}
250+ })
134251
135− session := &sessions.SessionState{AccessToken: "gitlab_access_token"}
136− err := p.EnrichSession(context.Background(), session)
137− assert.Error(t, err)
138−}
252+ Context("when generating group list from multiple kind", func() {
253+ type entitiesTableInput struct {
254+ projects []string
255+ groups []string
256+ }
257+
258+ DescribeTable("should prefix entities with group kind", func(in entitiesTableInput) {
259+ p.Groups = in.groups
260+ err := p.AddProjects(in.projects)
261+ Expect(err).To(BeNil())
262+
263+ all := p.PrefixAllowedGroups()
264+
265+ Expect(len(all)).To(Equal(len(in.projects) + len(in.groups)))
266+ },
267+ Entry("simple test case", entitiesTableInput{
268+ projects: []string{"my_group/my_project", "my_group/my_other_project"},
269+ groups: []string{"mygroup", "myothergroup"},
270+ }),
271+ Entry("projects only", entitiesTableInput{
272+ projects: []string{"my_group/my_project", "my_group/my_other_project"},
273+ groups: []string{},
274+ }),
275+ Entry("groups only", entitiesTableInput{
276+ projects: []string{},
277+ groups: []string{"mygroup", "myothergroup"},
278+ }),
279+ )
280+ })
281+})
providers/providers_suite_test.gonew file+16
@@ -0,0 +1,16 @@
1+package providers_test
2+
3+import (
4+ "testing"
5+
6+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
7+ . "github.com/onsi/ginkgo"
8+ . "github.com/onsi/gomega"
9+)
10+
11+func TestProviderSuite(t *testing.T) {
12+ logger.SetOutput(GinkgoWriter)
13+
14+ RegisterFailHandler(Fail)
15+ RunSpecs(t, "Providers")
16+}