Merge remote-tracking branch 'origin2/master'

This commit is contained in:
Kevin Kreitner 2020-12-21 09:38:45 +01:00
commit 3b90bf3f2a
10 changed files with 530 additions and 161 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

@ -54,7 +54,7 @@ Read the docs on our [Docs site](https://oauth2-proxy.github.io/oauth2-proxy/doc
## Getting Involved
If you would like to reach out to the maintainers, come talk to us in the `#oauth2_proxy` channel in the [Gophers slack](http://gophers.slack.com/).
If you would like to reach out to the maintainers, come talk to us in the `#oauth2-proxy` channel in the [Gophers slack](http://gophers.slack.com/).
## Contributing

View File

@ -0,0 +1,3 @@
# oauth2-proxy: local-environment
Run `make up` to deploy local dex, etcd and oauth2-proxy instances in Docker containers. Review the [`Makefile`](Makefile) for additional deployment options.

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)
```
@ -186,27 +188,66 @@ Take note of your `TenantId` if applicable for your situation. The `TenantId` ca
### OpenID Connect Provider
OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects. This provider was originally built against CoreOS Dex and we will use it as an example.
OpenID Connect is a spec for OAUTH 2.0 + identity that is implemented by many major providers and several open source projects.
1. Launch a Dex instance using the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md).
2. Setup oauth2-proxy with the correct provider and using the default ports and callbacks.
3. Login with the fixture use in the dex guide and run the oauth2-proxy with the following args:
This provider was originally built against CoreOS Dex and we will use it as an example.
The OpenID Connect Provider (OIDC) can also be used to connect to other Identity Providers such as Okta, an example can be found below.
```
#### Dex
To configure the OIDC provider for Dex, perform the following steps:
1. Download Dex:
```
go get github.com/dexidp/dex
```
See the [getting started guide](https://github.com/coreos/dex/blob/master/Documentation/getting-started.md) for more details.
2. Setup oauth2-proxy with the correct provider and using the default ports and callbacks. Add a configuration block to the `staticClients` section of `examples/config-dev.yaml`:
```
- id: oauth2-proxy
redirectURIs:
- 'http://127.0.0.1:4180/oauth2/callback'
name: 'oauth2-proxy'
secret: proxy
```
3. Launch Dex: from `$GOPATH/github.com/dexidp/dex`, run:
```
bin/dex serve examples/config-dev.yaml
```
4. In a second terminal, run the oauth2-proxy with the following args:
```
-provider oidc
-provider-display-name "My OIDC Provider"
-client-id oauth2-proxy
-client-secret proxy
-redirect-url http://127.0.0.1:4180/oauth2/callback
-oidc-issuer-url http://127.0.0.1:5556
-oidc-issuer-url http://127.0.0.1:5556/dex
-cookie-secure=false
-email-domain example.com
```
-cookie-secret=secret
-email-domain kilgore.trout
```
The OpenID Connect Provider (OIDC) can also be used to connect to other Identity Providers such as Okta. To configure the OIDC provider for Okta, perform
the following steps:
To serve the current working directory as a web site under the `/static` endpoint, add:
#### Configuring the OIDC Provider with Okta
```
-upstream file://$PWD/#/static/
```
5. Test the setup by visiting http://127.0.0.1:4180 or http://127.0.0.1:4180/static .
See also [our local testing environment](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/contrib/local-environment) for a self-contained example using Docker and etcd as storage for Dex.
#### Okta
To configure the OIDC provider for Okta, perform the following steps:
1. Log in to Okta using an administrative account. It is suggested you try this in preview first, `example.oktapreview.com`
2. (OPTIONAL) If you want to configure authorization scopes and claims to be passed on to multiple applications,
@ -229,30 +270,31 @@ you may wish to configure an authorization server for each application. Otherwis
* Under **Assignments** select the users or groups you wish to access your application.
4. Create a configuration file like the following:
```
provider = "oidc"
redirect_url = "https://example.corp.com/oauth2/callback"
oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234"
upstreams = [
"https://example.corp.com"
]
email_domains = [
"corp.com"
]
client_id = "XXXXX"
client_secret = "YYYYY"
pass_access_token = true
cookie_secret = "ZZZZZ"
skip_provider_button = true
```
```
provider = "oidc"
redirect_url = "https://example.corp.com/oauth2/callback"
oidc_issuer_url = "https://corp.okta.com/oauth2/abCd1234"
upstreams = [
"https://example.corp.com"
]
email_domains = [
"corp.com"
]
client_id = "XXXXX"
client_secret = "YYYYY"
pass_access_token = true
cookie_secret = "ZZZZZ"
skip_provider_button = true
```
The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Issuer** field in step 2, or simply https://corp.okta.com
The `oidc_issuer_url` is based on URL from your **Authorization Server**'s **Issuer** field in step 2, or simply https://corp.okta.com .
The `client_id` and `client_secret` are configured in the application settings.
Generate a unique `client_secret` to encrypt the cookie.
Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/example.cfg`
#### Configuring the OIDC Provider with Okta - localhost
#### Okta - localhost
1. Signup for developer account: https://developer.okta.com/signup/
2. Create New `Web` Application: https://${your-okta-domain}/dev/console/apps/new
3. Example Application Settings for localhost:
@ -265,25 +307,25 @@ Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/example.c
4. Make note of the `Client ID` and `Client secret`, they are needed in a future step
5. Make note of the **default** Authorization Server Issuer URI from: https://${your-okta-domain}/admin/oauth2/as
6. Example config file `/etc/localhost.cfg`
```
provider = "oidc"
redirect_url = "http://localhost:4180/oauth2/callback"
oidc_issuer_url = "https://${your-okta-domain}/oauth2/default"
upstreams = [
"http://0.0.0.0:8080"
]
email_domains = [
"*"
]
client_id = "XXX"
client_secret = "YYY"
pass_access_token = true
cookie_secret = "ZZZ"
cookie_secure = false
skip_provider_button = true
# Note: use the following for testing within a container
# http_address = "0.0.0.0:4180"
```
```
provider = "oidc"
redirect_url = "http://localhost:4180/oauth2/callback"
oidc_issuer_url = "https://${your-okta-domain}/oauth2/default"
upstreams = [
"http://0.0.0.0:8080"
]
email_domains = [
"*"
]
client_id = "XXX"
client_secret = "YYY"
pass_access_token = true
cookie_secret = "ZZZ"
cookie_secure = false
skip_provider_button = true
# Note: use the following for testing within a container
# http_address = "0.0.0.0:4180"
```
7. Then you can start the oauth2-proxy with `./oauth2-proxy --config /etc/localhost.cfg`
### login.gov Provider

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

@ -29,7 +29,7 @@ func Validate(o *options.Options) error {
msgs = append(msgs, validateSessionCookieMinimal(o)...)
msgs = append(msgs, validateRedisSessionStore(o)...)
msgs = append(msgs, prefixValues("injectRequestHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
msgs = append(msgs, prefixValues("injectRespeonseHeaders: ", validateHeaders(o.InjectRequestHeaders)...)...)
msgs = append(msgs, prefixValues("injectResponseHeaders: ", validateHeaders(o.InjectResponseHeaders)...)...)
if o.SSLInsecureSkipVerify {
// InsecureSkipVerify is a configurable option we allow
@ -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")
}