diff --git a/CHANGELOG.md b/CHANGELOG.md index 330b49de2e..2f6065b4de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,14 @@ ## Important Notes +- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Gitlab projects needs a Gitlab application with the extra `read_api` enabled - [#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. - [#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. - [#797](https://github.com/oauth2-proxy/oauth2-proxy/pull/797) The behavior of the Google provider Groups restriction changes with this - Either `--google-group` or the new `--allowed-group` will work for Google now (`--google-group` will be used if both are set) - Group membership lists will be passed to the backend with the `X-Forwarded-Groups` header - If you change the list of allowed groups, existing sessions that now don't have a valid group will be logged out immediately. - - Previously, group membership was only checked on session creation and refresh. + - Previously, group membership was only checked on session creation and refresh. - [#789](https://github.com/oauth2-proxy/oauth2-proxy/pull/789) `--skip-auth-route` is (almost) backwards compatible with `--skip-auth-regex` - We are marking `--skip-auth-regex` as DEPRECATED and will remove it in the next major version. - 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 @@ be any redirects in the browser anymore when tokens expire, but instead a token refresh is initiated in the background, which leads to new tokens being returned in the cookies. - Please note that `--cookie-refresh` must be 0 (the default) or equal to the token lifespan configured in Azure AD to make - Azure token refresh reliable. Setting this value to 0 means that it relies on the provider implementation - to decide if a refresh is required. + Azure token refresh reliable. Setting this value to 0 means that it relies on the provider implementation + to decide if a refresh is required. ## Changes since v6.1.1 +- [#630](https://github.com/oauth2-proxy/oauth2-proxy/pull/630) Add support for Gitlab project based authentication (@factorysh) - [#907](https://github.com/oauth2-proxy/oauth2-proxy/pull/907) Introduce alpha configuration option to enable testing of structured configuration (@JoelSpeed) - [#938](https://github.com/oauth2-proxy/oauth2-proxy/pull/938) Cleanup missed provider renaming refactor methods (@NickMeves) - [#925](https://github.com/oauth2-proxy/oauth2-proxy/pull/925) Fix basic auth legacy header conversion (@JoelSpeed) @@ -78,7 +80,6 @@ - [#829](https://github.com/oauth2-proxy/oauth2-proxy/pull/820) Rename test directory to testdata (@johejo) - [#819](https://github.com/oauth2-proxy/oauth2-proxy/pull/819) Improve CI (@johejo) - # v6.1.1 ## Release Highlights @@ -180,7 +181,7 @@ N/A - [#440](https://github.com/oauth2-proxy/oauth2-proxy/pull/440) Switch Azure AD Graph API to Microsoft Graph API - 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. 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. - Existing behaviour can be retained by setting `-resource=https://graph.windows.net`. + Existing behaviour can be retained by setting `-resource=https://graph.windows.net`. - [#484](https://github.com/oauth2-proxy/oauth2-proxy/pull/484) Configuration loading has been replaced with Viper and PFlag - Flags now require a `--` prefix before the option - Previously flags allowed either `-` or `--` to prefix the option name @@ -201,7 +202,7 @@ N/A - [#556](https://github.com/oauth2-proxy/oauth2-proxy/pull/556) Remove unintentional auto-padding of secrets that were too short - Previously, after cookie-secrets were opportunistically base64 decoded to raw bytes, they were padded to have a length divisible by 4. - - This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors + - This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors reporting an invalid length of 20 or 28 when the user input cookie-secret was not that length. - 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 the direct cookie-secret to bytes without silent padding added. @@ -306,15 +307,18 @@ N/A # v5.1.0 ## Release Highlights + - Bump to Go 1.14 - Reduced number of Google API requests for group validation - Support for Redis Cluster - Support for overriding hosts in hosts file ## Important Notes + - [#335] The session expiry for the OIDC provider is now taken from the Token Response (expires_in) rather than from the id_token (exp) ## Breaking Changes + N/A ## Changes since v5.0.0 @@ -338,13 +342,15 @@ N/A # v5.0.0 ## Release Highlights + - Disabled CGO (binaries will work regardless og glibc/musl) - Allow whitelisted redirect ports - Nextcloud provider support added - DigitalOcean provider support added ## Important Notes -- (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 + +- (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 ## Breaking Changes @@ -365,6 +371,7 @@ N/A # v4.1.0 ## Release Highlights + - Added Keycloak provider - Build on Go 1.13 - Upgrade Docker image to use Debian Buster @@ -373,12 +380,15 @@ N/A - Added support for GitHub teams ## Important Notes + N/A ## Breaking Changes + N/A ## Changes since v4.0.0 + - [#292](https://github.com/oauth2-proxy/oauth2-proxy/pull/292) Added bash >= 4.0 dependency to configure script (@jmfrank63) - [#227](https://github.com/oauth2-proxy/oauth2-proxy/pull/227) Add Keycloak provider (@Ofinka) - [#259](https://github.com/oauth2-proxy/oauth2-proxy/pull/259) Redirect to HTTPS (@jmickey) @@ -401,6 +411,7 @@ N/A # v4.0.0 ## Release Highlights + - Documentation is now on a [microsite](https://oauth2-proxy.github.io/oauth2-proxy/) - Health check logging can now be disabled for quieter logs - Authorization Header JWTs can now be verified by the proxy to skip authentication for machine users @@ -408,29 +419,30 @@ N/A - Logging overhaul allows customisable logging formats ## Important Notes + - This release includes a number of breaking changes that will require users to -reconfigure their proxies. Please read the Breaking Changes below thoroughly. + reconfigure their proxies. Please read the Breaking Changes below thoroughly. ## Breaking Changes - [#231](https://github.com/oauth2-proxy/oauth2-proxy/pull/231) Rework GitLab provider - This PR changes the configuration options for the GitLab provider to use - a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than - explicit `-login-url`, `-redeem-url` and `-validate-url` parameters. + a self-hosted instance. You now need to specify a `-oidc-issuer-url` rather than + explicit `-login-url`, `-redeem-url` and `-validate-url` parameters. - [#186](https://github.com/oauth2-proxy/oauth2-proxy/pull/186) Make config consistent - This PR changes configuration options so that all flags have a config counterpart - of the same name but with underscores (`_`) in place of hyphens (`-`). - This change affects the following flags: + of the same name but with underscores (`_`) in place of hyphens (`-`). + This change affects the following flags: - The `--tls-key` flag is now `--tls-key-file` to be consistent with existing - file flags and the existing config and environment settings + file flags and the existing config and environment settings - The `--tls-cert` flag is now `--tls-cert-file` to be consistent with existing - file flags and the existing config and environment settings - This change affects the following existing configuration options: + file flags and the existing config and environment settings + This change affects the following existing configuration options: - The `proxy-prefix` option is now `proxy_prefix`. - This PR changes environment variables so that all flags have an environment - counterpart of the same name but capitalised, with underscores (`_`) in place - of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`. - This change affects the following existing environment variables: + This PR changes environment variables so that all flags have an environment + counterpart of the same name but capitalised, with underscores (`_`) in place + of hyphens (`-`) and with the prefix `OAUTH2_PROXY_`. + This change affects the following existing environment variables: - The `OAUTH2_SKIP_OIDC_DISCOVERY` environment variable is now `OAUTH2_PROXY_SKIP_OIDC_DISCOVERY`. - The `OAUTH2_OIDC_JWKS_URL` environment variable is now `OAUTH2_PROXY_OIDC_JWKS_URL`. - [#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. - [#65](https://github.com/oauth2-proxy/oauth2-proxy/pull/65) Improvements to authenticate requests with a JWT bearer token in the `Authorization` header via the `-skip-jwt-bearer-token` options. (@brianv0) - 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 - (e.g. `https://example.com/.well-known/jwks.json`). + (e.g. `https://example.com/.well-known/jwks.json`). - [#180](https://github.com/oauth2-proxy/oauth2-proxy/pull/180) Minor refactor of core proxying path (@aeijdenberg). - [#175](https://github.com/oauth2-proxy/oauth2-proxy/pull/175) Bump go-oidc to v2.0.0 (@aeijdenberg). - 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. # v3.2.0 ## Release highlights + - Internal restructure of session state storage to use JSON rather than proprietary scheme - Added health check options for running on GCP behind a load balancer - Improved support for protecting websockets @@ -521,9 +534,10 @@ reconfigure their proxies. Please read the Breaking Changes below thoroughly. - Allow manual configuration of OIDC providers ## Important notes + - Dockerfile user is now non-root, this may break your existing deployment - In the OIDC provider, when no email is returned, the ID Token subject will be used -instead of returning an error + instead of returning an error - GitHub user emails must now be primary and verified before authenticating ## Changes since v3.1.0 diff --git a/docs/docs/configuration/auth.md b/docs/docs/configuration/auth.md index e05c0edb2a..30973efc02 100644 --- a/docs/docs/configuration/auth.md +++ b/docs/docs/configuration/auth.md @@ -149,6 +149,8 @@ The group management in keycloak is using a tree. If you create a group named ad Whether 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. +If you need projects filtering, add the extra `read_api` scope to your application. + The 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) ``` diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md index 76e7400fca..46e72d6770 100644 --- a/docs/docs/configuration/overview.md +++ b/docs/docs/configuration/overview.md @@ -54,6 +54,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/ | `--github-token` | string | the token to use when verifying repository collaborators (must have push access to the repository) | | | `--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 | | | `--gitlab-group` | string \| list | restrict logins to members of any of these groups (slug), separated by a comma | | +| `--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 | | | `--google-admin-email` | string | the google admin to impersonate for api calls | | | `--google-group` | string | restrict logins to members of this google group (may be given multiple times). | | | `--google-service-account-json` | string | the path to the service account json credentials | | diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go index 6cb95f541a..cf4e241457 100644 --- a/pkg/apis/options/options.go +++ b/pkg/apis/options/options.go @@ -48,6 +48,7 @@ type Options struct { GitHubToken string `flag:"github-token" cfg:"github_token"` GitHubUsers []string `flag:"github-user" cfg:"github_users"` GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"` + GitlabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"` GoogleGroups []string `flag:"google-group" cfg:"google_group"` GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"` GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"` @@ -188,6 +189,7 @@ func NewFlagSet() *pflag.FlagSet { flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)") 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)") flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)") + 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") flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).") flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") flagSet.String("google-service-account-json", "", "the path to the service account json credentials") diff --git a/pkg/validation/options.go b/pkg/validation/options.go index 15cd374e18..a13d846940 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -282,6 +282,12 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { case *providers.GitLabProvider: p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail p.Groups = o.GitLabGroup + err := p.AddProjects(o.GitlabProjects) + if err != nil { + msgs = append(msgs, "failed to setup gitlab project access level") + } + p.SetAllowedGroups(p.PrefixAllowedGroups()) + p.SetProjectScope() if p.Verifier == nil { // Initialize with default verifier for gitlab.com diff --git a/providers/gitlab.go b/providers/gitlab.go index 3941235c0c..246dd78c9f 100644 --- a/providers/gitlab.go +++ b/providers/gitlab.go @@ -3,10 +3,13 @@ package providers import ( "context" "fmt" + "net/url" + "strconv" + "strings" "time" - oidc "github.com/coreos/go-oidc" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests" "golang.org/x/oauth2" ) @@ -15,11 +18,54 @@ import ( type GitLabProvider struct { *ProviderData - Groups []string - Verifier *oidc.IDTokenVerifier + Groups []string + Projects []*GitlabProject + AllowUnverifiedEmail bool } +// GitlabProject represents a Gitlab project constraint entity +type GitlabProject struct { + Name string + AccessLevel int +} + +// newGitlabProject Creates a new GitlabProject struct from project string formatted as namespace/project=accesslevel +// if no accesslevel provided, use the default one +func newGitlabproject(project string) (*GitlabProject, error) { + // default access level is 20 + defaultAccessLevel := 20 + // see https://docs.gitlab.com/ee/api/members.html#valid-access-levels + validAccessLevel := [4]int{10, 20, 30, 40} + + parts := strings.SplitN(project, "=", 2) + + if len(parts) == 2 { + lvl, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + for _, valid := range validAccessLevel { + if lvl == valid { + return &GitlabProject{ + Name: parts[0], + AccessLevel: lvl}, + err + } + } + + return nil, fmt.Errorf("invalid gitlab project access level specified (%s)", parts[0]) + + } + + return &GitlabProject{ + Name: project, + AccessLevel: defaultAccessLevel}, + nil + +} + var _ Provider = (*GitLabProvider)(nil) const ( @@ -64,6 +110,19 @@ func (p *GitLabProvider) Redeem(ctx context.Context, redirectURL, code string) ( return } +// SetProjectScope ensure read_api is added to scope when filtering on projects +func (p *GitLabProvider) SetProjectScope() { + if len(p.Projects) > 0 { + for _, val := range strings.Split(p.Scope, " ") { + if val == "read_api" { + return + } + + } + p.Scope += " read_api" + } +} + // RefreshSessionIfNeeded checks if the session has expired and uses the // RefreshToken to fetch a new ID token if required func (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 return &userInfo, nil } -func (p *GitLabProvider) verifyGroupMembership(userInfo *gitlabUserInfo) error { - if len(p.Groups) == 0 { - return nil +type gitlabPermissionAccess struct { + AccessLevel int `json:"access_level"` +} + +type gitlabProjectPermission struct { + ProjectAccess *gitlabPermissionAccess `json:"project_access"` + GroupAccess *gitlabPermissionAccess `json:"group_access"` +} + +type gitlabProjectInfo struct { + Name string `json:"name"` + Archived bool `json:"archived"` + PathWithNamespace string `json:"path_with_namespace"` + Permissions gitlabProjectPermission `json:"permissions"` +} + +func (p *GitLabProvider) getProjectInfo(ctx context.Context, s *sessions.SessionState, project string) (*gitlabProjectInfo, error) { + var projectInfo gitlabProjectInfo + + endpointURL := &url.URL{ + Scheme: p.LoginURL.Scheme, + Host: p.LoginURL.Host, + Path: "/api/v4/projects/", } - // Collect user group memberships - membershipSet := make(map[string]bool) - for _, group := range userInfo.Groups { - membershipSet[group] = true + err := requests.New(fmt.Sprintf("%s%s", endpointURL.String(), url.QueryEscape(project))). + WithContext(ctx). + SetHeader("Authorization", "Bearer "+s.AccessToken). + Do(). + UnmarshalInto(&projectInfo) + + if err != nil { + return nil, fmt.Errorf("failed to get project info: %v", err) } - // Find a valid group that they are a member of - for _, validGroup := range p.Groups { - if _, ok := membershipSet[validGroup]; ok { - return nil + return &projectInfo, nil +} + +// AddProjects adds Gitlab projects from options to GitlabProvider struct +func (p *GitLabProvider) AddProjects(projects []string) error { + for _, project := range projects { + gp, err := newGitlabproject(project) + if err != nil { + return err } + + p.Projects = append(p.Projects, gp) } - return fmt.Errorf("user is not a member of '%s'", p.Groups) + return nil } func (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 return err == nil } -// GetEmailAddress returns the Account email address +// EnrichSession adds values and data from the Gitlab endpoint to current session func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { // Retrieve user info userInfo, err := p.getUserInfo(ctx, s) @@ -206,15 +296,67 @@ func (p *GitLabProvider) EnrichSession(ctx context.Context, s *sessions.SessionS return fmt.Errorf("user email is not verified") } - // Check group membership - // TODO (@NickMeves) - Refactor to Authorize - err = p.verifyGroupMembership(userInfo) - if err != nil { - return fmt.Errorf("group membership check failed: %v", err) - } - s.User = userInfo.Username s.Email = userInfo.Email + p.addGroupsToSession(ctx, s) + + p.addProjectsToSession(ctx, s) + return nil + +} + +// addGroupsToSession projects into session.Groups +func (p *GitLabProvider) addGroupsToSession(ctx context.Context, s *sessions.SessionState) { + // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user + for _, group := range p.Groups { + s.Groups = append(s.Groups, fmt.Sprintf("group:%s", group)) + } +} + +// addProjectsToSession adds projects matching user access requirements into the session state groups list +// This method prefix projects names with `project` to specify group kind +func (p *GitLabProvider) addProjectsToSession(ctx context.Context, s *sessions.SessionState) { + // Iterate over projects, check if oauth2-proxy can get project information on behalf of the user + for _, project := range p.Projects { + projectInfo, err := p.getProjectInfo(ctx, s, project.Name) + + if err != nil { + logger.Errorf("Warning: project info request failed: %v", err) + continue + } + + if !projectInfo.Archived { + perms := projectInfo.Permissions.ProjectAccess + if perms == nil { + // use group project access as fallback + perms = projectInfo.Permissions.GroupAccess + } + + if perms.AccessLevel >= project.AccessLevel { + s.Groups = append(s.Groups, fmt.Sprintf("project:%s", project.Name)) + } else { + logger.Errorf("Warning: user %q does not have the minimum required access level for project %q", s.Email, project.Name) + } + } else { + logger.Errorf("Warning: project %s is archived", project.Name) + } + + } + +} + +// PrefixAllowedGroups returns a list of allowed groups, prefixed by their `kind` value +func (p *GitLabProvider) PrefixAllowedGroups() (groups []string) { + + for _, val := range p.Groups { + groups = append(groups, fmt.Sprintf("group:%s", val)) + } + + for _, val := range p.Projects { + groups = append(groups, fmt.Sprintf("project:%s", val.Name)) + } + + return groups } diff --git a/providers/gitlab_test.go b/providers/gitlab_test.go index e3d974bf3a..62f0a70a30 100644 --- a/providers/gitlab_test.go +++ b/providers/gitlab_test.go @@ -2,13 +2,15 @@ package providers import ( "context" + "errors" "net/http" "net/http/httptest" "net/url" - "testing" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" - "github.com/stretchr/testify/assert" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" ) func testGitLabProvider(hostname string) *GitLabProvider { @@ -39,100 +41,241 @@ func testGitLabBackend() *httptest.Server { "groups": ["foo", "bar"] } ` + projectInfo := ` + { + "name": "MyProject", + "archived": false, + "path_with_namespace": "my_group/my_project", + "permissions": { + "project_access": null, + "group_access": { + "access_level": 30, + "notification_level": 3 + } + } + } + ` + + personalProjectInfo := ` + { + "name": "MyPersonalProject", + "archived": false, + "path_with_namespace": "my_profile/my_personal_project", + "permissions": { + "project_access": { + "access_level": 30, + "notification_level": 3 + }, + "group_access": null + } + } + ` + + archivedProjectInfo := ` + { + "name": "MyArchivedProject", + "archived": true, + "path_with_namespace": "my_group/my_archived_project", + "permissions": { + "project_access": { + "access_level": 30, + "notification_level": 3 + }, + "group_access": null + } + } + ` + authHeader := "Bearer gitlab_access_token" return httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/oauth/userinfo" { + switch r.URL.Path { + case "/oauth/userinfo": if r.Header["Authorization"][0] == authHeader { w.WriteHeader(200) w.Write([]byte(userInfo)) } else { w.WriteHeader(401) } - } else { + case "/api/v4/projects/my_group/my_project": + if r.Header["Authorization"][0] == authHeader { + w.WriteHeader(200) + w.Write([]byte(projectInfo)) + } else { + w.WriteHeader(401) + } + case "/api/v4/projects/my_group/my_archived_project": + if r.Header["Authorization"][0] == authHeader { + w.WriteHeader(200) + w.Write([]byte(archivedProjectInfo)) + } else { + w.WriteHeader(401) + } + case "/api/v4/projects/my_profile/my_personal_project": + if r.Header["Authorization"][0] == authHeader { + w.WriteHeader(200) + w.Write([]byte(personalProjectInfo)) + } else { + w.WriteHeader(401) + } + case "/api/v4/projects/my_group/my_bad_project": + w.WriteHeader(403) + default: w.WriteHeader(404) } })) } -func TestGitLabProviderBadToken(t *testing.T) { - b := testGitLabBackend() - defer b.Close() +var _ = Describe("Gitlab Provider Tests", func() { + var p *GitLabProvider + var b *httptest.Server - bURL, _ := url.Parse(b.URL) - p := testGitLabProvider(bURL.Host) + BeforeEach(func() { + b = testGitLabBackend() - session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"} - err := p.EnrichSession(context.Background(), session) - assert.Error(t, err) -} + bURL, err := url.Parse(b.URL) + Expect(err).To(BeNil()) -func TestGitLabProviderUnverifiedEmailDenied(t *testing.T) { - b := testGitLabBackend() - defer b.Close() + p = testGitLabProvider(bURL.Host) + }) - bURL, _ := url.Parse(b.URL) - p := testGitLabProvider(bURL.Host) + AfterEach(func() { + b.Close() + }) - session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - err := p.EnrichSession(context.Background(), session) - assert.Error(t, err) -} + Context("with bad token", func() { + It("should trigger an error", func() { + p.AllowUnverifiedEmail = false + session := &sessions.SessionState{AccessToken: "unexpected_gitlab_access_token"} + err := p.EnrichSession(context.Background(), session) + Expect(err).To(MatchError(errors.New("failed to retrieve user info: error getting user info: unexpected status \"401\": "))) + }) + }) -func TestGitLabProviderUnverifiedEmailAllowed(t *testing.T) { - b := testGitLabBackend() - defer b.Close() + Context("when filtering on email", func() { + type emailsTableInput struct { + expectedError error + expectedValue string + allowUnverifiedEmail bool + } - bURL, _ := url.Parse(b.URL) - p := testGitLabProvider(bURL.Host) - p.AllowUnverifiedEmail = true + DescribeTable("should return expected results", + func(in emailsTableInput) { + p.AllowUnverifiedEmail = in.allowUnverifiedEmail + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - err := p.EnrichSession(context.Background(), session) - assert.NoError(t, err) - assert.Equal(t, "foo@bar.com", session.Email) -} + err := p.EnrichSession(context.Background(), session) -func TestGitLabProviderUsername(t *testing.T) { - b := testGitLabBackend() - defer b.Close() + if in.expectedError != nil { + Expect(err).To(MatchError(err)) + } else { + Expect(err).To(BeNil()) + Expect(session.Email).To(Equal(in.expectedValue)) + } + }, + Entry("unverified email denied", emailsTableInput{ + expectedError: errors.New("user email is not verified"), + allowUnverifiedEmail: false, + }), + Entry("unverified email allowed", emailsTableInput{ + expectedError: nil, + expectedValue: "foo@bar.com", + allowUnverifiedEmail: true, + }), + ) + }) - bURL, _ := url.Parse(b.URL) - p := testGitLabProvider(bURL.Host) - p.AllowUnverifiedEmail = true + Context("when filtering on gitlab entities (groups and projects)", func() { + type entitiesTableInput struct { + expectedValue []string + projects []string + groups []string + } - session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - err := p.EnrichSession(context.Background(), session) - assert.NoError(t, err) - assert.Equal(t, "FooBar", session.User) -} + DescribeTable("should return expected results", + func(in entitiesTableInput) { + p.AllowUnverifiedEmail = true + session := &sessions.SessionState{AccessToken: "gitlab_access_token"} -func TestGitLabProviderGroupMembershipValid(t *testing.T) { - b := testGitLabBackend() - defer b.Close() + err := p.AddProjects(in.projects) + Expect(err).To(BeNil()) + p.SetProjectScope() - bURL, _ := url.Parse(b.URL) - p := testGitLabProvider(bURL.Host) - p.AllowUnverifiedEmail = true - p.Groups = []string{"foo"} + if len(in.groups) > 0 { + p.Groups = in.groups + } - session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - err := p.EnrichSession(context.Background(), session) - assert.NoError(t, err) - assert.Equal(t, "FooBar", session.User) -} + err = p.EnrichSession(context.Background(), session) -func TestGitLabProviderGroupMembershipMissing(t *testing.T) { - b := testGitLabBackend() - defer b.Close() + Expect(err).To(BeNil()) + Expect(session.Groups).To(Equal(in.expectedValue)) + }, + Entry("project membership valid on group project", entitiesTableInput{ + expectedValue: []string{"project:my_group/my_project"}, + projects: []string{"my_group/my_project"}, + }), + Entry("project membership invalid on group project, insufficient access level level", entitiesTableInput{ + expectedValue: nil, + projects: []string{"my_group/my_project=40"}, + }), + Entry("project membership valid on personnal project", entitiesTableInput{ + expectedValue: []string{"project:my_profile/my_personal_project"}, + projects: []string{"my_profile/my_personal_project"}, + }), + Entry("project membership invalid on personnal project, insufficient access level", entitiesTableInput{ + expectedValue: nil, + projects: []string{"my_profile/my_personal_project=40"}, + }), + Entry("project membership invalid", entitiesTableInput{ + expectedValue: nil, + projects: []string{"my_group/my_bad_project"}, + }), + Entry("group membership valid", entitiesTableInput{ + expectedValue: []string{"group:foo"}, + groups: []string{"foo"}, + }), + Entry("groups and projects", entitiesTableInput{ + expectedValue: []string{"group:foo", "group:baz", "project:my_group/my_project", "project:my_profile/my_personal_project"}, + groups: []string{"foo", "baz"}, + projects: []string{"my_group/my_project", "my_profile/my_personal_project"}, + }), + Entry("archived projects", entitiesTableInput{ + expectedValue: nil, + groups: []string{}, + projects: []string{"my_group/my_archived_project"}, + }), + ) - bURL, _ := url.Parse(b.URL) - p := testGitLabProvider(bURL.Host) - p.AllowUnverifiedEmail = true - p.Groups = []string{"baz"} + }) - session := &sessions.SessionState{AccessToken: "gitlab_access_token"} - err := p.EnrichSession(context.Background(), session) - assert.Error(t, err) -} + Context("when generating group list from multiple kind", func() { + type entitiesTableInput struct { + projects []string + groups []string + } + + DescribeTable("should prefix entities with group kind", func(in entitiesTableInput) { + p.Groups = in.groups + err := p.AddProjects(in.projects) + Expect(err).To(BeNil()) + + all := p.PrefixAllowedGroups() + + Expect(len(all)).To(Equal(len(in.projects) + len(in.groups))) + }, + Entry("simple test case", entitiesTableInput{ + projects: []string{"my_group/my_project", "my_group/my_other_project"}, + groups: []string{"mygroup", "myothergroup"}, + }), + Entry("projects only", entitiesTableInput{ + projects: []string{"my_group/my_project", "my_group/my_other_project"}, + groups: []string{}, + }), + Entry("groups only", entitiesTableInput{ + projects: []string{}, + groups: []string{"mygroup", "myothergroup"}, + }), + ) + }) +}) diff --git a/providers/providers_suite_test.go b/providers/providers_suite_test.go new file mode 100644 index 0000000000..6a7f8b1f87 --- /dev/null +++ b/providers/providers_suite_test.go @@ -0,0 +1,16 @@ +package providers_test + +import ( + "testing" + + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestProviderSuite(t *testing.T) { + logger.SetOutput(GinkgoWriter) + + RegisterFailHandler(Fail) + RunSpecs(t, "Providers") +}