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:
parent
5117f2314f
commit
d67d6e3152
56
CHANGELOG.md
56
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
|
||||
|
@ -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)
|
||||
|
||||
```
|
||||
|
@ -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 | |
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
16
providers/providers_suite_test.go
Normal file
16
providers/providers_suite_test.go
Normal 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")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user