1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-03-21 21:47:11 +02:00

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>
This commit is contained in:
Mathieu Lecarme 2020-12-05 19:57:33 +01:00 committed by GitHub
parent 5117f2314f
commit d67d6e3152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 437 additions and 111 deletions

View File

@ -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

View File

@ -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)
```

View File

@ -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 | |

View File

@ -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")

View File

@ -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

View File

@ -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
}

View File

@ -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"},
}),
)
})
})

View File

@ -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")
}