forked from mystiq/dex
Merge branch 'master' into feature/static_password_env
This commit is contained in:
commit
9560899496
46 changed files with 897 additions and 279 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -69,8 +69,8 @@ jobs:
|
||||||
DEX_KEYSTONE_ADMIN_USER: demo
|
DEX_KEYSTONE_ADMIN_USER: demo
|
||||||
DEX_KEYSTONE_ADMIN_PASS: DEMO_PASS
|
DEX_KEYSTONE_ADMIN_PASS: DEMO_PASS
|
||||||
|
|
||||||
- name: Run Kubernetes tests
|
- name: Run linter
|
||||||
run: ./scripts/test-k8s.sh
|
run: make lint
|
||||||
|
|
||||||
# Ensure proto generation doesn't depend on external packages.
|
# Ensure proto generation doesn't depend on external packages.
|
||||||
- name: Verify proto
|
- name: Verify proto
|
||||||
|
|
35
.golangci.yml
Normal file
35
.golangci.yml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
run:
|
||||||
|
skip-dirs:
|
||||||
|
- vendor
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
golint:
|
||||||
|
min-confidence: 0.1
|
||||||
|
goimports:
|
||||||
|
local-prefixes: github.com/dexidp/dex
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- funlen
|
||||||
|
- maligned
|
||||||
|
- wsl
|
||||||
|
|
||||||
|
# TODO: fix me
|
||||||
|
- unparam
|
||||||
|
- golint
|
||||||
|
- goconst
|
||||||
|
- staticcheck
|
||||||
|
- nakedret
|
||||||
|
- errcheck
|
||||||
|
- gosec
|
||||||
|
- gochecknoinits
|
||||||
|
- gochecknoglobals
|
||||||
|
- prealloc
|
||||||
|
- scopelint
|
||||||
|
- lll
|
||||||
|
- dupl
|
||||||
|
- gocritic
|
||||||
|
- gocyclo
|
||||||
|
- gocognit
|
||||||
|
- godox
|
|
@ -48,7 +48,6 @@ install:
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- make testall
|
- make testall
|
||||||
- ./scripts/test-k8s.sh
|
|
||||||
- make verify-proto # Ensure proto generation doesn't depend on external packages.
|
- make verify-proto # Ensure proto generation doesn't depend on external packages.
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
|
|
79
Documentation/connectors/openshift.md
Normal file
79
Documentation/connectors/openshift.md
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# Authentication using OpenShift
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Dex can make use of users and groups defined within OpenShift by querying the platform provided OAuth server.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
|
||||||
|
### Creating an OAuth Client
|
||||||
|
|
||||||
|
Two forms of OAuth Clients can be utilized:
|
||||||
|
|
||||||
|
* [Using a Service Account as an OAuth Client](https://docs.openshift.com/container-platform/latest/authentication/using-service-accounts-as-oauth-client.html) (Recommended)
|
||||||
|
* [Registering An Additional OAuth Client](https://docs.openshift.com/container-platform/latest/authentication/configuring-internal-oauth.html#oauth-register-additional-client_configuring-internal-oauth)
|
||||||
|
|
||||||
|
#### Using a Service Account as an OAuth Client
|
||||||
|
|
||||||
|
OpenShift Service Accounts can be used as a constrained form of OAuth client. Making use of a Service Account to represent an OAuth Client is the recommended option as it does not require elevated privileged within the OpenShift cluster. Create a new Service Account or make use of an existing Service Account.
|
||||||
|
|
||||||
|
Patch the Service Account to add an annotation for location of the Redirect URI
|
||||||
|
|
||||||
|
```
|
||||||
|
oc patch serviceaccount <name> --type='json' -p='[{"op": "add", "path": "/metadata/annotations/serviceaccounts.openshift.io~1oauth-redirecturi.dex", "value":"https:///<dex_url>/callback"}]'
|
||||||
|
```
|
||||||
|
|
||||||
|
The Client ID for a Service Account representing an OAuth Client takes the form `
|
||||||
|
|
||||||
|
The Client Secret for a Service Account representing an OAuth Client is the long lived OAuth Token that is configued for the Service Account. Execute the following command to retrieve the OAuth Token.
|
||||||
|
|
||||||
|
```
|
||||||
|
oc serviceaccounts get-token <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Registering An Additional OAuth Client
|
||||||
|
|
||||||
|
Instead of using a constrained form of Service Account to represent an OAuth Client, an additional OAuthClient resource can be created.
|
||||||
|
|
||||||
|
Create a new OAuthClient resource similar to the following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
kind: OAuthClient
|
||||||
|
apiVersion: oauth.openshift.io/v1
|
||||||
|
metadata:
|
||||||
|
name: dex
|
||||||
|
# The value that should be utilized as the `client_secret`
|
||||||
|
secret: "<clientSecret>"
|
||||||
|
# List of valid addresses for the callback. Ensure one of the values that are provided is `(dex issuer)/callback`
|
||||||
|
redirectURIs:
|
||||||
|
- "https:///<dex_url>/callback"
|
||||||
|
grantMethod: prompt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dex Configuration
|
||||||
|
|
||||||
|
The following is an example of a configuration for `examples/config-dev.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
connectors:
|
||||||
|
- type: openshift
|
||||||
|
# Required field for connector id.
|
||||||
|
id: openshift
|
||||||
|
# Required field for connector name.
|
||||||
|
name: OpenShift
|
||||||
|
config:
|
||||||
|
# OpenShift API
|
||||||
|
issuer: https://api.mycluster.example.com:6443
|
||||||
|
# Credentials can be string literals or pulled from the environment.
|
||||||
|
clientID: $OPENSHIFT_OAUTH_CLIENT_ID
|
||||||
|
clientSecret: $OPENSHIFT_OAUTH_CLIENT_SECRET
|
||||||
|
redirectURI: http://127.0.0.1:5556/dex/
|
||||||
|
# Optional: Specify whether to communicate to OpenShift without validating SSL ceertificates
|
||||||
|
insecureCA: false
|
||||||
|
# Optional: The location of file containing SSL certificates to commmunicate to OpenShift
|
||||||
|
rootCA: /etc/ssl/openshift.pem
|
||||||
|
# Optional list of required groups a user mmust be a member of
|
||||||
|
groups:
|
||||||
|
- users
|
||||||
|
```
|
|
@ -1,21 +1,5 @@
|
||||||
# Running integration tests
|
# Running integration tests
|
||||||
|
|
||||||
## Kubernetes
|
|
||||||
|
|
||||||
Kubernetes tests run against a Kubernetes API server, and are enabled by the `DEX_KUBECONFIG` environment variable:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ export DEX_KUBECONFIG=~/.kube/config
|
|
||||||
$ go test -v -i ./storage/kubernetes
|
|
||||||
$ go test -v ./storage/kubernetes
|
|
||||||
```
|
|
||||||
|
|
||||||
These tests can be executed locally using docker by running the following script:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./scripts/test-k8s.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Postgres
|
## Postgres
|
||||||
|
|
||||||
Running database tests locally requires:
|
Running database tests locally requires:
|
||||||
|
|
|
@ -14,11 +14,13 @@ Steps:
|
||||||
```yaml
|
```yaml
|
||||||
frontend:
|
frontend:
|
||||||
dir: /path/to/custom/web
|
dir: /path/to/custom/web
|
||||||
|
issuer: my-dex
|
||||||
extra:
|
extra:
|
||||||
tos_footer_link: "https://example.com/terms"
|
tos_footer_link: "https://example.com/terms"
|
||||||
client_logo_url: "../theme/client-logo.png"
|
client_logo_url: "../theme/client-logo.png"
|
||||||
foo: "bar"
|
foo: "bar"
|
||||||
```
|
```
|
||||||
5. Set the `frontend.dir` value to your own `web` directory.
|
5. Set the `frontend.dir` value to your own `web` directory.
|
||||||
|
6. Write the issuer in the `issuer` directory in order to modify the Dex title and the `Log in to <<dex>>` tag.
|
||||||
|
|
||||||
To test your templates simply run Dex with a valid configuration and go through a login flow.
|
To test your templates simply run Dex with a valid configuration and go through a login flow.
|
45
Makefile
45
Makefile
|
@ -2,7 +2,6 @@ PROJ=dex
|
||||||
ORG_PATH=github.com/dexidp
|
ORG_PATH=github.com/dexidp
|
||||||
REPO_PATH=$(ORG_PATH)/$(PROJ)
|
REPO_PATH=$(ORG_PATH)/$(PROJ)
|
||||||
export PATH := $(PWD)/bin:$(PATH)
|
export PATH := $(PWD)/bin:$(PATH)
|
||||||
THIS_DIRECTORY:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
|
||||||
|
|
||||||
VERSION ?= $(shell ./scripts/git-version)
|
VERSION ?= $(shell ./scripts/git-version)
|
||||||
|
|
||||||
|
@ -18,6 +17,9 @@ export GOBIN=$(PWD)/bin
|
||||||
|
|
||||||
LD_FLAGS="-w -X $(REPO_PATH)/version.Version=$(VERSION)"
|
LD_FLAGS="-w -X $(REPO_PATH)/version.Version=$(VERSION)"
|
||||||
|
|
||||||
|
# Dependency versions
|
||||||
|
GOLANGCI_VERSION = 1.21.0
|
||||||
|
|
||||||
build: bin/dex bin/example-app bin/grpc-client
|
build: bin/dex bin/example-app bin/grpc-client
|
||||||
|
|
||||||
bin/dex:
|
bin/dex:
|
||||||
|
@ -39,20 +41,38 @@ revendor:
|
||||||
@go mod vendor -v
|
@go mod vendor -v
|
||||||
@go mod verify
|
@go mod verify
|
||||||
|
|
||||||
test:
|
test: bin/test/kube-apiserver bin/test/etcd
|
||||||
@go test -v ./...
|
@go test -v ./...
|
||||||
|
|
||||||
testrace:
|
testrace: bin/test/kube-apiserver bin/test/etcd
|
||||||
@go test -v --race ./...
|
@go test -v --race ./...
|
||||||
|
|
||||||
vet:
|
export TEST_ASSET_KUBE_APISERVER=$(abspath bin/test/kube-apiserver)
|
||||||
@go vet ./...
|
export TEST_ASSET_ETCD=$(abspath bin/test/etcd)
|
||||||
|
|
||||||
fmt:
|
bin/test/kube-apiserver:
|
||||||
@./scripts/gofmt ./...
|
@mkdir -p bin/test
|
||||||
|
curl -L https://storage.googleapis.com/k8s-c10s-test-binaries/kube-apiserver-$(shell uname)-x86_64 > bin/test/kube-apiserver
|
||||||
|
chmod +x bin/test/kube-apiserver
|
||||||
|
|
||||||
lint: bin/golint
|
bin/test/etcd:
|
||||||
@./bin/golint -set_exit_status $(shell go list ./...)
|
@mkdir -p bin/test
|
||||||
|
curl -L https://storage.googleapis.com/k8s-c10s-test-binaries/etcd-$(shell uname)-x86_64 > bin/test/etcd
|
||||||
|
chmod +x bin/test/etcd
|
||||||
|
|
||||||
|
bin/golangci-lint: bin/golangci-lint-${GOLANGCI_VERSION}
|
||||||
|
@ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint
|
||||||
|
bin/golangci-lint-${GOLANGCI_VERSION}:
|
||||||
|
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | BINARY=golangci-lint bash -s -- v${GOLANGCI_VERSION}
|
||||||
|
@mv bin/golangci-lint $@
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: bin/golangci-lint ## Run linter
|
||||||
|
bin/golangci-lint run
|
||||||
|
|
||||||
|
.PHONY: fix
|
||||||
|
fix: bin/golangci-lint ## Fix lint violations
|
||||||
|
bin/golangci-lint run --fix
|
||||||
|
|
||||||
.PHONY: docker-image
|
.PHONY: docker-image
|
||||||
docker-image:
|
docker-image:
|
||||||
|
@ -73,14 +93,11 @@ bin/protoc: scripts/get-protoc
|
||||||
bin/protoc-gen-go:
|
bin/protoc-gen-go:
|
||||||
@go install -v $(REPO_PATH)/vendor/github.com/golang/protobuf/protoc-gen-go
|
@go install -v $(REPO_PATH)/vendor/github.com/golang/protobuf/protoc-gen-go
|
||||||
|
|
||||||
bin/golint:
|
|
||||||
@go install -v $(THIS_DIRECTORY)/vendor/golang.org/x/lint/golint
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -rf bin/
|
@rm -rf bin/
|
||||||
|
|
||||||
testall: testrace vet fmt lint
|
testall: testrace
|
||||||
|
|
||||||
FORCE:
|
FORCE:
|
||||||
|
|
||||||
.PHONY: test testrace vet fmt lint testall
|
.PHONY: test testrace testall
|
||||||
|
|
|
@ -74,6 +74,7 @@ Dex implements the following connectors:
|
||||||
| [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | |
|
| [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | |
|
||||||
| [AuthProxy](Documentation/connectors/authproxy.md) | no | no | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. |
|
| [AuthProxy](Documentation/connectors/authproxy.md) | no | no | no | alpha | Authentication proxies such as Apache2 mod_auth, etc. |
|
||||||
| [Bitbucket Cloud](Documentation/connectors/bitbucketcloud.md) | yes | yes | no | alpha | |
|
| [Bitbucket Cloud](Documentation/connectors/bitbucketcloud.md) | yes | yes | no | alpha | |
|
||||||
|
| [OpenShift](Documentation/connectors/openshift.md) | no | yes | no | stable | |
|
||||||
|
|
||||||
Stable, beta, and alpha are defined as:
|
Stable, beta, and alpha are defined as:
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector/mock"
|
"github.com/dexidp/dex/connector/mock"
|
||||||
"github.com/dexidp/dex/connector/oidc"
|
"github.com/dexidp/dex/connector/oidc"
|
||||||
|
"github.com/dexidp/dex/server"
|
||||||
"github.com/dexidp/dex/storage"
|
"github.com/dexidp/dex/storage"
|
||||||
"github.com/dexidp/dex/storage/sql"
|
"github.com/dexidp/dex/storage/sql"
|
||||||
)
|
)
|
||||||
|
@ -215,7 +216,6 @@ logger:
|
||||||
if diff := pretty.Compare(c, want); diff != "" {
|
if diff := pretty.Compare(c, want); diff != "" {
|
||||||
t.Errorf("got!=want: %s", diff)
|
t.Errorf("got!=want: %s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalConfigWithEnv(t *testing.T) {
|
func TestUnmarshalConfigWithEnv(t *testing.T) {
|
||||||
|
|
|
@ -182,7 +182,6 @@ func serve(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("failed to initialize storage connectors: %v", err)
|
return fmt.Errorf("failed to initialize storage connectors: %v", err)
|
||||||
}
|
}
|
||||||
storageConnectors[i] = conn
|
storageConnectors[i] = conn
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.EnablePasswordDB {
|
if c.EnablePasswordDB {
|
||||||
|
|
|
@ -143,7 +143,7 @@ func cmd() *cobra.Command {
|
||||||
ctx := oidc.ClientContext(context.Background(), a.client)
|
ctx := oidc.ClientContext(context.Background(), a.client)
|
||||||
provider, err := oidc.NewProvider(ctx, issuerURL)
|
provider, err := oidc.NewProvider(ctx, issuerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to query provider %q: %v", issuerURL, err)
|
return fmt.Errorf("failed to query provider %q: %v", issuerURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var s struct {
|
var s struct {
|
||||||
|
@ -153,7 +153,7 @@ func cmd() *cobra.Command {
|
||||||
ScopesSupported []string `json:"scopes_supported"`
|
ScopesSupported []string `json:"scopes_supported"`
|
||||||
}
|
}
|
||||||
if err := provider.Claims(&s); err != nil {
|
if err := provider.Claims(&s); err != nil {
|
||||||
return fmt.Errorf("Failed to parse provider scopes_supported: %v", err)
|
return fmt.Errorf("failed to parse provider scopes_supported: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.ScopesSupported) == 0 {
|
if len(s.ScopesSupported) == 0 {
|
||||||
|
|
|
@ -41,7 +41,6 @@ type Config struct {
|
||||||
|
|
||||||
// Open returns a strategy for logging in through Bitbucket.
|
// Open returns a strategy for logging in through Bitbucket.
|
||||||
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
|
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
|
||||||
|
|
||||||
b := bitbucketConnector{
|
b := bitbucketConnector{
|
||||||
redirectURI: c.RedirectURI,
|
redirectURI: c.RedirectURI,
|
||||||
teams: c.Teams,
|
teams: c.Teams,
|
||||||
|
@ -373,7 +372,6 @@ type userTeamsResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *bitbucketConnector) userTeams(ctx context.Context, client *http.Client) ([]string, error) {
|
func (b *bitbucketConnector) userTeams(ctx context.Context, client *http.Client) ([]string, error) {
|
||||||
|
|
||||||
var teams []string
|
var teams []string
|
||||||
apiURL := b.apiURL + "/teams?role=member"
|
apiURL := b.apiURL + "/teams?role=member"
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUserGroups(t *testing.T) {
|
func TestUserGroups(t *testing.T) {
|
||||||
|
|
||||||
teamsResponse := userTeamsResponse{
|
teamsResponse := userTeamsResponse{
|
||||||
pagedResponse: pagedResponse{
|
pagedResponse: pagedResponse{
|
||||||
Size: 3,
|
Size: 3,
|
||||||
|
@ -46,7 +45,6 @@ func TestUserGroups(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserWithoutTeams(t *testing.T) {
|
func TestUserWithoutTeams(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]interface{}{
|
s := newTestServer(map[string]interface{}{
|
||||||
"/teams?role=member": userTeamsResponse{},
|
"/teams?role=member": userTeamsResponse{},
|
||||||
})
|
})
|
||||||
|
@ -61,7 +59,6 @@ func TestUserWithoutTeams(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]interface{}{
|
s := newTestServer(map[string]interface{}{
|
||||||
"/user": user{Username: "some-login"},
|
"/user": user{Username: "some-login"},
|
||||||
"/user/emails": userEmailResponse{
|
"/user/emails": userEmailResponse{
|
||||||
|
|
|
@ -67,7 +67,6 @@ type Org struct {
|
||||||
|
|
||||||
// Open returns a strategy for logging in through GitHub.
|
// Open returns a strategy for logging in through GitHub.
|
||||||
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
|
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
|
||||||
|
|
||||||
if c.Org != "" {
|
if c.Org != "" {
|
||||||
// Return error if both 'org' and 'orgs' fields are used.
|
// Return error if both 'org' and 'orgs' fields are used.
|
||||||
if len(c.Orgs) > 0 {
|
if len(c.Orgs) > 0 {
|
||||||
|
@ -107,7 +106,6 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
|
||||||
if g.httpClient, err = newHTTPClient(g.rootCA); err != nil {
|
if g.httpClient, err = newHTTPClient(g.rootCA); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
|
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
g.loadAllGroups = c.LoadAllGroups
|
g.loadAllGroups = c.LoadAllGroups
|
||||||
|
|
||||||
|
@ -144,7 +142,7 @@ type githubConnector struct {
|
||||||
hostName string
|
hostName string
|
||||||
// Used to support untrusted/self-signed CA certs.
|
// Used to support untrusted/self-signed CA certs.
|
||||||
rootCA string
|
rootCA string
|
||||||
// HTTP Client that trusts the custom delcared rootCA cert.
|
// HTTP Client that trusts the custom declared rootCA cert.
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
// optional choice between 'name' (default) or 'slug'
|
// optional choice between 'name' (default) or 'slug'
|
||||||
teamNameField string
|
teamNameField string
|
||||||
|
@ -206,7 +204,7 @@ func (e *oauth2Error) Error() string {
|
||||||
return e.error + ": " + e.errorDescription
|
return e.error + ": " + e.errorDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
// newHTTPClient returns a new HTTP client that trusts the custom delcared rootCA cert.
|
// newHTTPClient returns a new HTTP client that trusts the custom declared rootCA cert.
|
||||||
func newHTTPClient(rootCA string) (*http.Client, error) {
|
func newHTTPClient(rootCA string) (*http.Client, error) {
|
||||||
tlsConfig := tls.Config{RootCAs: x509.NewCertPool()}
|
tlsConfig := tls.Config{RootCAs: x509.NewCertPool()}
|
||||||
rootCABytes, err := ioutil.ReadFile(rootCA)
|
rootCABytes, err := ioutil.ReadFile(rootCA)
|
||||||
|
|
|
@ -126,7 +126,6 @@ func TestUserGroupsWithTeamNameAndSlugFieldConfig(t *testing.T) {
|
||||||
|
|
||||||
// tests that the users login is used as their username when they have no username set
|
// tests that the users login is used as their username when they have no username set
|
||||||
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]testResponse{
|
s := newTestServer(map[string]testResponse{
|
||||||
"/user": {data: user{Login: "some-login", ID: 12345678}},
|
"/user": {data: user{Login: "some-login", ID: 12345678}},
|
||||||
"/user/emails": {data: []userEmail{{
|
"/user/emails": {data: []userEmail{{
|
||||||
|
@ -168,7 +167,6 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
|
func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]testResponse{
|
s := newTestServer(map[string]testResponse{
|
||||||
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
|
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
|
||||||
"/user/emails": {data: []userEmail{{
|
"/user/emails": {data: []userEmail{{
|
||||||
|
|
|
@ -65,7 +65,6 @@ func TestUserGroupsWithoutOrgs(t *testing.T) {
|
||||||
|
|
||||||
// tests that the email is used as their username when they have no username set
|
// tests that the email is used as their username when they have no username set
|
||||||
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]interface{}{
|
s := newTestServer(map[string]interface{}{
|
||||||
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678},
|
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678},
|
||||||
"/oauth/token": map[string]interface{}{
|
"/oauth/token": map[string]interface{}{
|
||||||
|
@ -102,7 +101,6 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
|
func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]interface{}{
|
s := newTestServer(map[string]interface{}{
|
||||||
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"},
|
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"},
|
||||||
"/oauth/token": map[string]interface{}{
|
"/oauth/token": map[string]interface{}{
|
||||||
|
@ -130,7 +128,6 @@ func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginWithTeamWhitelisted(t *testing.T) {
|
func TestLoginWithTeamWhitelisted(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]interface{}{
|
s := newTestServer(map[string]interface{}{
|
||||||
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs"},
|
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs"},
|
||||||
"/oauth/token": map[string]interface{}{
|
"/oauth/token": map[string]interface{}{
|
||||||
|
@ -158,7 +155,6 @@ func TestLoginWithTeamWhitelisted(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginWithTeamNonWhitelisted(t *testing.T) {
|
func TestLoginWithTeamNonWhitelisted(t *testing.T) {
|
||||||
|
|
||||||
s := newTestServer(map[string]interface{}{
|
s := newTestServer(map[string]interface{}{
|
||||||
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"},
|
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"},
|
||||||
"/oauth/token": map[string]interface{}{
|
"/oauth/token": map[string]interface{}{
|
||||||
|
|
|
@ -11,12 +11,12 @@ import (
|
||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
|
admin "google.golang.org/api/admin/directory/v1"
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector"
|
"github.com/dexidp/dex/connector"
|
||||||
pkg_groups "github.com/dexidp/dex/pkg/groups"
|
pkg_groups "github.com/dexidp/dex/pkg/groups"
|
||||||
"github.com/dexidp/dex/pkg/log"
|
"github.com/dexidp/dex/pkg/log"
|
||||||
"golang.org/x/oauth2/google"
|
|
||||||
admin "google.golang.org/api/admin/directory/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -105,7 +105,6 @@ type googleConnector struct {
|
||||||
redirectURI string
|
redirectURI string
|
||||||
oauth2Config *oauth2.Config
|
oauth2Config *oauth2.Config
|
||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
hostedDomains []string
|
hostedDomains []string
|
||||||
|
|
|
@ -150,7 +150,6 @@ func (p *conn) Prompt() string { return "username" }
|
||||||
|
|
||||||
func (p *conn) Refresh(
|
func (p *conn) Refresh(
|
||||||
ctx context.Context, scopes connector.Scopes, identity connector.Identity) (connector.Identity, error) {
|
ctx context.Context, scopes connector.Scopes, identity connector.Identity) (connector.Identity, error) {
|
||||||
|
|
||||||
token, err := p.getAdminToken(ctx)
|
token, err := p.getAdminToken(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return identity, fmt.Errorf("keystone: failed to obtain admin token: %v", err)
|
return identity, fmt.Errorf("keystone: failed to obtain admin token: %v", err)
|
||||||
|
@ -210,6 +209,8 @@ func (p *conn) getAdminToken(ctx context.Context) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
token := resp.Header.Get("X-Subject-Token")
|
token := resp.Header.Get("X-Subject-Token")
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
@ -229,6 +230,7 @@ func (p *conn) checkIfUserExists(ctx context.Context, userID string, token strin
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode == 200 {
|
if resp.StatusCode == 200 {
|
||||||
return true, nil
|
return true, nil
|
||||||
|
|
|
@ -154,7 +154,12 @@ func delete(t *testing.T, token, id, uri string) {
|
||||||
t.Fatalf("error: %v", err)
|
t.Fatalf("error: %v", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("X-Auth-Token", token)
|
req.Header.Set("X-Auth-Token", token)
|
||||||
client.Do(req)
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGroup(t *testing.T, token, description, name string) string {
|
func createGroup(t *testing.T, token, description, name string) string {
|
||||||
|
@ -208,7 +213,13 @@ func addUserToGroup(t *testing.T, token, groupID, userID string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("X-Auth-Token", token)
|
req.Header.Set("X-Auth-Token", token)
|
||||||
client.Do(req)
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,7 +285,7 @@ func TestUseRefreshToken(t *testing.T) {
|
||||||
delete(t, token, groupID, groupsURL)
|
delete(t, token, groupID, groupsURL)
|
||||||
|
|
||||||
expectEquals(t, 1, len(identityRefresh.Groups))
|
expectEquals(t, 1, len(identityRefresh.Groups))
|
||||||
expectEquals(t, testGroup, string(identityRefresh.Groups[0]))
|
expectEquals(t, testGroup, identityRefresh.Groups[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUseRefreshTokenUserDeleted(t *testing.T) {
|
func TestUseRefreshTokenUserDeleted(t *testing.T) {
|
||||||
|
|
|
@ -189,7 +189,6 @@ func (c *Config) OpenConnector(logger log.Logger) (interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) {
|
func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) {
|
||||||
|
|
||||||
requiredFields := []struct {
|
requiredFields := []struct {
|
||||||
name string
|
name string
|
||||||
val string
|
val string
|
||||||
|
@ -365,7 +364,6 @@ func (c *ldapConnector) identityFromEntry(user ldap.Entry) (ident connector.Iden
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.Entry, found bool, err error) {
|
func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.Entry, found bool, err error) {
|
||||||
|
|
||||||
filter := fmt.Sprintf("(%s=%s)", c.UserSearch.Username, ldap.EscapeFilter(username))
|
filter := fmt.Sprintf("(%s=%s)", c.UserSearch.Username, ldap.EscapeFilter(username))
|
||||||
if c.UserSearch.Filter != "" {
|
if c.UserSearch.Filter != "" {
|
||||||
filter = fmt.Sprintf("(&%s%s)", c.UserSearch.Filter, filter)
|
filter = fmt.Sprintf("(&%s%s)", c.UserSearch.Filter, filter)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
|
@ -85,18 +84,6 @@ func knownBrokenAuthHeaderProvider(issuerURL string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// golang.org/x/oauth2 doesn't do internal locking. Need to do it in this
|
|
||||||
// package ourselves and hope that other packages aren't calling it at the
|
|
||||||
// same time.
|
|
||||||
var registerMu = new(sync.Mutex)
|
|
||||||
|
|
||||||
func registerBrokenAuthHeaderProvider(url string) {
|
|
||||||
registerMu.Lock()
|
|
||||||
defer registerMu.Unlock()
|
|
||||||
|
|
||||||
oauth2.RegisterBrokenAuthHeaderProvider(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open returns a connector which can be used to login users through an upstream
|
// Open returns a connector which can be used to login users through an upstream
|
||||||
// OpenID Connect provider.
|
// OpenID Connect provider.
|
||||||
func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
|
func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
|
||||||
|
@ -108,13 +95,15 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
|
||||||
return nil, fmt.Errorf("failed to get provider: %v", err)
|
return nil, fmt.Errorf("failed to get provider: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint := provider.Endpoint()
|
||||||
|
|
||||||
if c.BasicAuthUnsupported != nil {
|
if c.BasicAuthUnsupported != nil {
|
||||||
// Setting "basicAuthUnsupported" always overrides our detection.
|
// Setting "basicAuthUnsupported" always overrides our detection.
|
||||||
if *c.BasicAuthUnsupported {
|
if *c.BasicAuthUnsupported {
|
||||||
registerBrokenAuthHeaderProvider(provider.Endpoint().TokenURL)
|
endpoint.AuthStyle = oauth2.AuthStyleInParams
|
||||||
}
|
}
|
||||||
} else if knownBrokenAuthHeaderProvider(c.Issuer) {
|
} else if knownBrokenAuthHeaderProvider(c.Issuer) {
|
||||||
registerBrokenAuthHeaderProvider(provider.Endpoint().TokenURL)
|
endpoint.AuthStyle = oauth2.AuthStyleInParams
|
||||||
}
|
}
|
||||||
|
|
||||||
scopes := []string{oidc.ScopeOpenID}
|
scopes := []string{oidc.ScopeOpenID}
|
||||||
|
@ -131,7 +120,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
|
||||||
oauth2Config: &oauth2.Config{
|
oauth2Config: &oauth2.Config{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: c.ClientSecret,
|
ClientSecret: c.ClientSecret,
|
||||||
Endpoint: provider.Endpoint(),
|
Endpoint: endpoint,
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
RedirectURL: c.RedirectURI,
|
RedirectURL: c.RedirectURI,
|
||||||
},
|
},
|
||||||
|
@ -273,15 +262,25 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
|
||||||
if !found {
|
if !found {
|
||||||
return identity, fmt.Errorf("missing \"%s\" claim", userNameKey)
|
return identity, fmt.Errorf("missing \"%s\" claim", userNameKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasEmailScope := false
|
||||||
|
for _, s := range c.oauth2Config.Scopes {
|
||||||
|
if s == "email" {
|
||||||
|
hasEmailScope = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
email, found := claims["email"].(string)
|
email, found := claims["email"].(string)
|
||||||
if !found {
|
if !found && hasEmailScope {
|
||||||
return identity, errors.New("missing \"email\" claim")
|
return identity, errors.New("missing \"email\" claim")
|
||||||
}
|
}
|
||||||
|
|
||||||
emailVerified, found := claims["email_verified"].(bool)
|
emailVerified, found := claims["email_verified"].(bool)
|
||||||
if !found {
|
if !found {
|
||||||
if c.insecureSkipEmailVerified {
|
if c.insecureSkipEmailVerified {
|
||||||
emailVerified = true
|
emailVerified = true
|
||||||
} else {
|
} else if hasEmailScope {
|
||||||
return identity, errors.New("missing \"email_verified\" claim")
|
return identity, errors.New("missing \"email_verified\" claim")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
|
"github.com/dexidp/dex/connector"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKnownBrokenAuthHeaderProvider(t *testing.T) {
|
func TestKnownBrokenAuthHeaderProvider(t *testing.T) {
|
||||||
|
@ -49,8 +50,10 @@ func TestHandleCallback(t *testing.T) {
|
||||||
userIDKey string
|
userIDKey string
|
||||||
userNameKey string
|
userNameKey string
|
||||||
insecureSkipEmailVerified bool
|
insecureSkipEmailVerified bool
|
||||||
|
scopes []string
|
||||||
expectUserID string
|
expectUserID string
|
||||||
expectUserName string
|
expectUserName string
|
||||||
|
expectedEmailField string
|
||||||
token map[string]interface{}
|
token map[string]interface{}
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -59,6 +62,7 @@ func TestHandleCallback(t *testing.T) {
|
||||||
userNameKey: "", // not configured
|
userNameKey: "", // not configured
|
||||||
expectUserID: "subvalue",
|
expectUserID: "subvalue",
|
||||||
expectUserName: "namevalue",
|
expectUserName: "namevalue",
|
||||||
|
expectedEmailField: "emailvalue",
|
||||||
token: map[string]interface{}{
|
token: map[string]interface{}{
|
||||||
"sub": "subvalue",
|
"sub": "subvalue",
|
||||||
"name": "namevalue",
|
"name": "namevalue",
|
||||||
|
@ -71,6 +75,7 @@ func TestHandleCallback(t *testing.T) {
|
||||||
insecureSkipEmailVerified: true,
|
insecureSkipEmailVerified: true,
|
||||||
expectUserID: "subvalue",
|
expectUserID: "subvalue",
|
||||||
expectUserName: "namevalue",
|
expectUserName: "namevalue",
|
||||||
|
expectedEmailField: "emailvalue",
|
||||||
token: map[string]interface{}{
|
token: map[string]interface{}{
|
||||||
"sub": "subvalue",
|
"sub": "subvalue",
|
||||||
"name": "namevalue",
|
"name": "namevalue",
|
||||||
|
@ -82,6 +87,7 @@ func TestHandleCallback(t *testing.T) {
|
||||||
userIDKey: "name",
|
userIDKey: "name",
|
||||||
expectUserID: "namevalue",
|
expectUserID: "namevalue",
|
||||||
expectUserName: "namevalue",
|
expectUserName: "namevalue",
|
||||||
|
expectedEmailField: "emailvalue",
|
||||||
token: map[string]interface{}{
|
token: map[string]interface{}{
|
||||||
"sub": "subvalue",
|
"sub": "subvalue",
|
||||||
"name": "namevalue",
|
"name": "namevalue",
|
||||||
|
@ -94,6 +100,7 @@ func TestHandleCallback(t *testing.T) {
|
||||||
userNameKey: "user_name",
|
userNameKey: "user_name",
|
||||||
expectUserID: "subvalue",
|
expectUserID: "subvalue",
|
||||||
expectUserName: "username",
|
expectUserName: "username",
|
||||||
|
expectedEmailField: "emailvalue",
|
||||||
token: map[string]interface{}{
|
token: map[string]interface{}{
|
||||||
"sub": "subvalue",
|
"sub": "subvalue",
|
||||||
"user_name": "username",
|
"user_name": "username",
|
||||||
|
@ -101,6 +108,33 @@ func TestHandleCallback(t *testing.T) {
|
||||||
"email_verified": true,
|
"email_verified": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "emptyEmailScope",
|
||||||
|
expectUserID: "subvalue",
|
||||||
|
expectUserName: "namevalue",
|
||||||
|
expectedEmailField: "",
|
||||||
|
scopes: []string{"groups"},
|
||||||
|
insecureSkipEmailVerified: true,
|
||||||
|
token: map[string]interface{}{
|
||||||
|
"sub": "subvalue",
|
||||||
|
"name": "namevalue",
|
||||||
|
"user_name": "username",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "emptyEmailScopeButEmailProvided",
|
||||||
|
expectUserID: "subvalue",
|
||||||
|
expectUserName: "namevalue",
|
||||||
|
expectedEmailField: "emailvalue",
|
||||||
|
scopes: []string{"groups"},
|
||||||
|
insecureSkipEmailVerified: true,
|
||||||
|
token: map[string]interface{}{
|
||||||
|
"sub": "subvalue",
|
||||||
|
"name": "namevalue",
|
||||||
|
"user_name": "username",
|
||||||
|
"email": "emailvalue",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
@ -110,16 +144,25 @@ func TestHandleCallback(t *testing.T) {
|
||||||
t.Fatal("failed to setup test server", err)
|
t.Fatal("failed to setup test server", err)
|
||||||
}
|
}
|
||||||
defer testServer.Close()
|
defer testServer.Close()
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if len(tc.scopes) > 0 {
|
||||||
|
scopes = tc.scopes
|
||||||
|
} else {
|
||||||
|
scopes = []string{"email", "groups"}
|
||||||
|
}
|
||||||
serverURL := testServer.URL
|
serverURL := testServer.URL
|
||||||
|
basicAuth := true
|
||||||
config := Config{
|
config := Config{
|
||||||
Issuer: serverURL,
|
Issuer: serverURL,
|
||||||
ClientID: "clientID",
|
ClientID: "clientID",
|
||||||
ClientSecret: "clientSecret",
|
ClientSecret: "clientSecret",
|
||||||
Scopes: []string{"groups"},
|
Scopes: scopes,
|
||||||
RedirectURI: fmt.Sprintf("%s/callback", serverURL),
|
RedirectURI: fmt.Sprintf("%s/callback", serverURL),
|
||||||
UserIDKey: tc.userIDKey,
|
UserIDKey: tc.userIDKey,
|
||||||
UserNameKey: tc.userNameKey,
|
UserNameKey: tc.userNameKey,
|
||||||
InsecureSkipEmailVerified: tc.insecureSkipEmailVerified,
|
InsecureSkipEmailVerified: tc.insecureSkipEmailVerified,
|
||||||
|
BasicAuthUnsupported: &basicAuth,
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := newConnector(config)
|
conn, err := newConnector(config)
|
||||||
|
@ -139,7 +182,7 @@ func TestHandleCallback(t *testing.T) {
|
||||||
|
|
||||||
expectEquals(t, identity.UserID, tc.expectUserID)
|
expectEquals(t, identity.UserID, tc.expectUserID)
|
||||||
expectEquals(t, identity.Username, tc.expectUserName)
|
expectEquals(t, identity.Username, tc.expectUserName)
|
||||||
expectEquals(t, identity.Email, "emailvalue")
|
expectEquals(t, identity.Email, tc.expectedEmailField)
|
||||||
expectEquals(t, identity.EmailVerified, true)
|
expectEquals(t, identity.EmailVerified, true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
254
connector/openshift/openshift.go
Normal file
254
connector/openshift/openshift.go
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
package openshift
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dexidp/dex/connector"
|
||||||
|
"github.com/dexidp/dex/pkg/groups"
|
||||||
|
"github.com/dexidp/dex/pkg/log"
|
||||||
|
|
||||||
|
"github.com/dexidp/dex/storage/kubernetes/k8sapi"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds configuration options for OpenShift login
|
||||||
|
type Config struct {
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
ClientID string `json:"clientID"`
|
||||||
|
ClientSecret string `json:"clientSecret"`
|
||||||
|
RedirectURI string `json:"redirectURI"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
InsecureCA bool `json:"insecureCA"`
|
||||||
|
RootCA string `json:"rootCA"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ connector.CallbackConnector = (*openshiftConnector)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type openshiftConnector struct {
|
||||||
|
apiURL string
|
||||||
|
redirectURI string
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
cancel context.CancelFunc
|
||||||
|
logger log.Logger
|
||||||
|
httpClient *http.Client
|
||||||
|
oauth2Config *oauth2.Config
|
||||||
|
insecureCA bool
|
||||||
|
rootCA string
|
||||||
|
groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
k8sapi.TypeMeta `json:",inline"`
|
||||||
|
k8sapi.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
Identities []string `json:"identities" protobuf:"bytes,3,rep,name=identities"`
|
||||||
|
FullName string `json:"fullName,omitempty" protobuf:"bytes,2,opt,name=fullName"`
|
||||||
|
Groups []string `json:"groups" protobuf:"bytes,4,rep,name=groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open returns a connector which can be used to login users through an upstream
|
||||||
|
// OpenShift OAuth2 provider.
|
||||||
|
func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
wellKnownURL := strings.TrimSuffix(c.Issuer, "/") + "/.well-known/oauth-authorization-server"
|
||||||
|
req, err := http.NewRequest(http.MethodGet, wellKnownURL, nil)
|
||||||
|
|
||||||
|
openshiftConnector := openshiftConnector{
|
||||||
|
apiURL: c.Issuer,
|
||||||
|
cancel: cancel,
|
||||||
|
clientID: c.ClientID,
|
||||||
|
clientSecret: c.ClientSecret,
|
||||||
|
insecureCA: c.InsecureCA,
|
||||||
|
logger: logger,
|
||||||
|
redirectURI: c.RedirectURI,
|
||||||
|
rootCA: c.RootCA,
|
||||||
|
groups: c.Groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
if openshiftConnector.httpClient, err = newHTTPClient(c.InsecureCA, c.RootCA); err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata struct {
|
||||||
|
Auth string `json:"authorization_endpoint"`
|
||||||
|
Token string `json:"token_endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := openshiftConnector.httpClient.Do(req.WithContext(ctx))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, fmt.Errorf("failed to query OpenShift endpoint %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, fmt.Errorf("discovery through endpoint %s failed to decode body: %v",
|
||||||
|
wellKnownURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
openshiftConnector.oauth2Config = &oauth2.Config{
|
||||||
|
ClientID: c.ClientID,
|
||||||
|
ClientSecret: c.ClientSecret,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: metadata.Auth, TokenURL: metadata.Token,
|
||||||
|
},
|
||||||
|
Scopes: []string{"user:info"},
|
||||||
|
RedirectURL: c.RedirectURI,
|
||||||
|
}
|
||||||
|
return &openshiftConnector, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *openshiftConnector) Close() error {
|
||||||
|
c.cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginURL returns the URL to redirect the user to login with.
|
||||||
|
func (c *openshiftConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) {
|
||||||
|
if c.redirectURI != callbackURL {
|
||||||
|
return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI)
|
||||||
|
}
|
||||||
|
return c.oauth2Config.AuthCodeURL(state), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type oauth2Error struct {
|
||||||
|
error string
|
||||||
|
errorDescription string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *oauth2Error) Error() string {
|
||||||
|
if e.errorDescription == "" {
|
||||||
|
return e.error
|
||||||
|
}
|
||||||
|
return e.error + ": " + e.errorDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCallback parses the request and returns the user's identity
|
||||||
|
func (c *openshiftConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
if errType := q.Get("error"); errType != "" {
|
||||||
|
return identity, &oauth2Error{errType, q.Get("error_description")}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
if c.httpClient != nil {
|
||||||
|
ctx = context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := c.oauth2Config.Exchange(ctx, q.Get("code"))
|
||||||
|
if err != nil {
|
||||||
|
return identity, fmt.Errorf("oidc: failed to get token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := c.oauth2Config.Client(ctx, token)
|
||||||
|
|
||||||
|
user, err := c.user(ctx, client)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return identity, fmt.Errorf("openshift: get user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.groups) > 0 {
|
||||||
|
validGroups := validateAllowedGroups(user.Groups, c.groups)
|
||||||
|
|
||||||
|
if !validGroups {
|
||||||
|
return identity, fmt.Errorf("openshift: user %q is not in any of the required groups", user.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
identity = connector.Identity{
|
||||||
|
UserID: user.UID,
|
||||||
|
Username: user.Name,
|
||||||
|
PreferredUsername: user.Name,
|
||||||
|
Groups: user.Groups,
|
||||||
|
}
|
||||||
|
|
||||||
|
return identity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// user function returns the OpenShift user associated with the authenticated user
|
||||||
|
func (c *openshiftConnector) user(ctx context.Context, client *http.Client) (u user, err error) {
|
||||||
|
url := c.apiURL + "/apis/user.openshift.io/v1/users/~"
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return u, fmt.Errorf("new req: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return u, fmt.Errorf("get URL %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return u, fmt.Errorf("read body: %v", err)
|
||||||
|
}
|
||||||
|
return u, fmt.Errorf("%s: %s", resp.Status, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
|
||||||
|
return u, fmt.Errorf("JSON decode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAllowedGroups(userGroups, allowedGroups []string) bool {
|
||||||
|
matchingGroups := groups.Filter(userGroups, allowedGroups)
|
||||||
|
|
||||||
|
return len(matchingGroups) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHTTPClient returns a new HTTP client
|
||||||
|
func newHTTPClient(insecureCA bool, rootCA string) (*http.Client, error) {
|
||||||
|
tlsConfig := tls.Config{}
|
||||||
|
|
||||||
|
if insecureCA {
|
||||||
|
tlsConfig = tls.Config{InsecureSkipVerify: true}
|
||||||
|
} else if rootCA != "" {
|
||||||
|
tlsConfig := tls.Config{RootCAs: x509.NewCertPool()}
|
||||||
|
rootCABytes, err := ioutil.ReadFile(rootCA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read root-ca: %v", err)
|
||||||
|
}
|
||||||
|
if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) {
|
||||||
|
return nil, fmt.Errorf("no certs found in root CA file %q", rootCA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tlsConfig,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
DualStack: true,
|
||||||
|
}).DialContext,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
218
connector/openshift/openshift_test.go
Normal file
218
connector/openshift/openshift_test.go
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
package openshift
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dexidp/dex/connector"
|
||||||
|
|
||||||
|
"github.com/dexidp/dex/storage/kubernetes/k8sapi"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpen(t *testing.T) {
|
||||||
|
s := newTestServer(map[string]interface{}{})
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
hostURL, err := url.Parse(s.URL)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
_, err = http.NewRequest("GET", hostURL.String(), nil)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
c := Config{
|
||||||
|
Issuer: s.URL,
|
||||||
|
ClientID: "testClientId",
|
||||||
|
ClientSecret: "testClientSecret",
|
||||||
|
RedirectURI: "https://localhost/callback",
|
||||||
|
InsecureCA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := logrus.New()
|
||||||
|
|
||||||
|
oconfig, err := c.Open("id", logger)
|
||||||
|
|
||||||
|
oc, ok := oconfig.(*openshiftConnector)
|
||||||
|
|
||||||
|
expectNil(t, err)
|
||||||
|
expectEquals(t, ok, true)
|
||||||
|
expectEquals(t, oc.apiURL, s.URL)
|
||||||
|
expectEquals(t, oc.clientID, "testClientId")
|
||||||
|
expectEquals(t, oc.clientSecret, "testClientSecret")
|
||||||
|
expectEquals(t, oc.redirectURI, "https://localhost/callback")
|
||||||
|
expectEquals(t, oc.oauth2Config.Endpoint.AuthURL, fmt.Sprintf("%s/oauth/authorize", s.URL))
|
||||||
|
expectEquals(t, oc.oauth2Config.Endpoint.TokenURL, fmt.Sprintf("%s/oauth/token", s.URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUser(t *testing.T) {
|
||||||
|
s := newTestServer(map[string]interface{}{
|
||||||
|
"/apis/user.openshift.io/v1/users/~": user{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "jdoe",
|
||||||
|
},
|
||||||
|
FullName: "John Doe",
|
||||||
|
Groups: []string{"users"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
hostURL, err := url.Parse(s.URL)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
_, err = http.NewRequest("GET", hostURL.String(), nil)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
h, err := newHTTPClient(true, "")
|
||||||
|
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
oc := openshiftConnector{apiURL: s.URL, httpClient: h}
|
||||||
|
u, err := oc.user(context.Background(), h)
|
||||||
|
|
||||||
|
expectNil(t, err)
|
||||||
|
expectEquals(t, u.Name, "jdoe")
|
||||||
|
expectEquals(t, u.FullName, "John Doe")
|
||||||
|
expectEquals(t, len(u.Groups), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifySingleGroupFn(t *testing.T) {
|
||||||
|
allowedGroups := []string{"users"}
|
||||||
|
groupMembership := []string{"users", "org1"}
|
||||||
|
|
||||||
|
validGroupMembership := validateAllowedGroups(groupMembership, allowedGroups)
|
||||||
|
|
||||||
|
expectEquals(t, validGroupMembership, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifySingleGroupFailureFn(t *testing.T) {
|
||||||
|
allowedGroups := []string{"admins"}
|
||||||
|
groupMembership := []string{"users"}
|
||||||
|
|
||||||
|
validGroupMembership := validateAllowedGroups(groupMembership, allowedGroups)
|
||||||
|
|
||||||
|
expectEquals(t, validGroupMembership, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyMultipleGroupFn(t *testing.T) {
|
||||||
|
allowedGroups := []string{"users", "admins"}
|
||||||
|
groupMembership := []string{"users", "org1"}
|
||||||
|
|
||||||
|
validGroupMembership := validateAllowedGroups(groupMembership, allowedGroups)
|
||||||
|
|
||||||
|
expectEquals(t, validGroupMembership, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyGroup(t *testing.T) {
|
||||||
|
s := newTestServer(map[string]interface{}{
|
||||||
|
"/apis/user.openshift.io/v1/users/~": user{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "jdoe",
|
||||||
|
},
|
||||||
|
FullName: "John Doe",
|
||||||
|
Groups: []string{"users"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
hostURL, err := url.Parse(s.URL)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
_, err = http.NewRequest("GET", hostURL.String(), nil)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
h, err := newHTTPClient(true, "")
|
||||||
|
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
oc := openshiftConnector{apiURL: s.URL, httpClient: h}
|
||||||
|
u, err := oc.user(context.Background(), h)
|
||||||
|
|
||||||
|
expectNil(t, err)
|
||||||
|
expectEquals(t, u.Name, "jdoe")
|
||||||
|
expectEquals(t, u.FullName, "John Doe")
|
||||||
|
expectEquals(t, len(u.Groups), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallbackIdentity(t *testing.T) {
|
||||||
|
s := newTestServer(map[string]interface{}{
|
||||||
|
"/apis/user.openshift.io/v1/users/~": user{
|
||||||
|
ObjectMeta: k8sapi.ObjectMeta{
|
||||||
|
Name: "jdoe",
|
||||||
|
UID: "12345",
|
||||||
|
},
|
||||||
|
FullName: "John Doe",
|
||||||
|
Groups: []string{"users"},
|
||||||
|
},
|
||||||
|
"/oauth/token": map[string]interface{}{
|
||||||
|
"access_token": "oRzxVjCnohYRHEYEhZshkmakKmoyVoTjfUGC",
|
||||||
|
"expires_in": "30",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
hostURL, err := url.Parse(s.URL)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", hostURL.String(), nil)
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
h, err := newHTTPClient(true, "")
|
||||||
|
|
||||||
|
expectNil(t, err)
|
||||||
|
|
||||||
|
oc := openshiftConnector{apiURL: s.URL, httpClient: h, oauth2Config: &oauth2.Config{
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: fmt.Sprintf("%s/oauth/authorize", s.URL),
|
||||||
|
TokenURL: fmt.Sprintf("%s/oauth/token", s.URL),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
identity, err := oc.HandleCallback(connector.Scopes{Groups: true}, req)
|
||||||
|
|
||||||
|
expectNil(t, err)
|
||||||
|
expectEquals(t, identity.UserID, "12345")
|
||||||
|
expectEquals(t, identity.Username, "jdoe")
|
||||||
|
expectEquals(t, identity.PreferredUsername, "jdoe")
|
||||||
|
expectEquals(t, len(identity.Groups), 1)
|
||||||
|
expectEquals(t, identity.Groups[0], "users")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestServer(responses map[string]interface{}) *httptest.Server {
|
||||||
|
var s *httptest.Server
|
||||||
|
s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
responses["/.well-known/oauth-authorization-server"] = map[string]interface{}{
|
||||||
|
"issuer": s.URL,
|
||||||
|
"authorization_endpoint": fmt.Sprintf("%s/oauth/authorize", s.URL),
|
||||||
|
"token_endpoint": fmt.Sprintf("%s/oauth/token", s.URL),
|
||||||
|
"scopes_supported": []string{"user:full", "user:info", "user:check-access", "user:list-scoped-projects", "user:list-projects"},
|
||||||
|
"response_types_supported": []string{"token", "code"},
|
||||||
|
"grant_types_supported": []string{"authorization_code", "implicit"},
|
||||||
|
"code_challenge_methods_supported": []string{"plain", "S256"},
|
||||||
|
}
|
||||||
|
|
||||||
|
response := responses[r.RequestURI]
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectNil(t *testing.T, a interface{}) {
|
||||||
|
if a != nil {
|
||||||
|
t.Errorf("Expected %+v to equal nil", a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectEquals(t *testing.T, a interface{}, b interface{}) {
|
||||||
|
if !reflect.DeepEqual(a, b) {
|
||||||
|
t.Errorf("Expected %+v to equal %+v", a, b)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,11 +14,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
|
dsig "github.com/russellhaering/goxmldsig"
|
||||||
|
"github.com/russellhaering/goxmldsig/etreeutils"
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector"
|
"github.com/dexidp/dex/connector"
|
||||||
"github.com/dexidp/dex/pkg/groups"
|
"github.com/dexidp/dex/pkg/groups"
|
||||||
"github.com/dexidp/dex/pkg/log"
|
"github.com/dexidp/dex/pkg/log"
|
||||||
dsig "github.com/russellhaering/goxmldsig"
|
|
||||||
"github.com/russellhaering/goxmldsig/etreeutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// nolint
|
// nolint
|
||||||
|
@ -248,7 +249,6 @@ type provider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) {
|
func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) {
|
||||||
|
|
||||||
r := &authnRequest{
|
r := &authnRequest{
|
||||||
ProtocolBinding: bindingPOST,
|
ProtocolBinding: bindingPOST,
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -325,7 +325,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
|
|
||||||
// Status is a required element.
|
// Status is a required element.
|
||||||
if resp.Status == nil {
|
if resp.Status == nil {
|
||||||
return ident, fmt.Errorf("Response did not contain a Status element")
|
return ident, fmt.Errorf("response did not contain a Status element")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = p.validateStatus(resp.Status); err != nil {
|
if err = p.validateStatus(resp.Status); err != nil {
|
||||||
|
@ -398,7 +398,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
|
|
||||||
if len(p.allowedGroups) > 0 && (!s.Groups || p.groupsAttr == "") {
|
if len(p.allowedGroups) > 0 && (!s.Groups || p.groupsAttr == "") {
|
||||||
// allowedGroups set but no groups or groupsAttr. Disallowing.
|
// allowedGroups set but no groups or groupsAttr. Disallowing.
|
||||||
return ident, fmt.Errorf("User not a member of allowed groups")
|
return ident, fmt.Errorf("user not a member of allowed groups")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab the groups.
|
// Grab the groups.
|
||||||
|
@ -427,7 +427,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
|
|
||||||
if len(groupMatches) == 0 {
|
if len(groupMatches) == 0 {
|
||||||
// No group membership matches found, disallowing
|
// No group membership matches found, disallowing
|
||||||
return ident, fmt.Errorf("User not a member of allowed groups")
|
return ident, fmt.Errorf("user not a member of allowed groups")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, we're good
|
// Otherwise, we're good
|
||||||
|
@ -468,7 +468,7 @@ func (p *provider) validateStatus(status *status) error {
|
||||||
func (p *provider) validateSubject(subject *subject, inResponseTo string) error {
|
func (p *provider) validateSubject(subject *subject, inResponseTo string) error {
|
||||||
// Optional according to the spec, but again, we're going to be strict here.
|
// Optional according to the spec, but again, we're going to be strict here.
|
||||||
if len(subject.SubjectConfirmations) == 0 {
|
if len(subject.SubjectConfirmations) == 0 {
|
||||||
return fmt.Errorf("Subject contained no SubjectConfirmations")
|
return fmt.Errorf("subject contained no SubjectConfirmations")
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -57,6 +57,7 @@ require (
|
||||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
||||||
gopkg.in/ldap.v2 v2.5.1
|
gopkg.in/ldap.v2 v2.5.1
|
||||||
gopkg.in/square/go-jose.v2 v2.3.1
|
gopkg.in/square/go-jose.v2 v2.3.1
|
||||||
|
sigs.k8s.io/testing_frameworks v0.1.2
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
13
go.sum
13
go.sum
|
@ -92,6 +92,7 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
|
@ -172,9 +173,11 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/onsi/ginkgo v1.4.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
|
@ -273,6 +276,7 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -300,6 +304,9 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
@ -317,6 +324,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
@ -363,6 +371,8 @@ gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUy
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
|
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
|
||||||
|
@ -373,6 +383,7 @@ gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
@ -385,3 +396,5 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
||||||
|
sigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH+UQM=
|
||||||
|
sigs.k8s.io/testing_frameworks v0.1.2/go.mod h1:ToQrwSC3s8Xf/lADdZp3Mktcql9CG0UAmdJG9th5i0w=
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
result=$( go fmt $@ )
|
|
||||||
if [[ $result != "" ]]; then
|
|
||||||
>&2 echo "The following files are not formatted correctly: $result"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
|
@ -1,56 +0,0 @@
|
||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
TEMPDIR=$( mktemp -d )
|
|
||||||
|
|
||||||
cat << EOF > $TEMPDIR/kubeconfig
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Config
|
|
||||||
clusters:
|
|
||||||
- name: local
|
|
||||||
cluster:
|
|
||||||
server: http://localhost:8080
|
|
||||||
users:
|
|
||||||
- name: local
|
|
||||||
user:
|
|
||||||
contexts:
|
|
||||||
- context:
|
|
||||||
cluster: local
|
|
||||||
user: local
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cleanup () {
|
|
||||||
docker rm -f $( cat $TEMPDIR/etcd )
|
|
||||||
docker rm -f $( cat $TEMPDIR/kube-apiserver )
|
|
||||||
rm -rf $TEMPDIR
|
|
||||||
}
|
|
||||||
|
|
||||||
trap "{ CODE=$?; cleanup ; exit $CODE; }" EXIT
|
|
||||||
|
|
||||||
docker run \
|
|
||||||
--cidfile=$TEMPDIR/etcd \
|
|
||||||
-d \
|
|
||||||
--net=host \
|
|
||||||
gcr.io/google_containers/etcd:3.1.10 \
|
|
||||||
etcd
|
|
||||||
|
|
||||||
docker run \
|
|
||||||
--cidfile=$TEMPDIR/kube-apiserver \
|
|
||||||
-d \
|
|
||||||
-v $TEMPDIR:/var/run/kube-test:ro \
|
|
||||||
--net=host \
|
|
||||||
gcr.io/google_containers/kube-apiserver-amd64:v1.7.4 \
|
|
||||||
kube-apiserver \
|
|
||||||
--etcd-servers=http://localhost:2379 \
|
|
||||||
--service-cluster-ip-range=10.0.0.1/16 \
|
|
||||||
--insecure-bind-address=0.0.0.0 \
|
|
||||||
--insecure-port=8080
|
|
||||||
|
|
||||||
until $(curl --output /dev/null --silent --head --fail http://localhost:8080/healthz); do
|
|
||||||
printf '.'
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo "API server ready"
|
|
||||||
|
|
||||||
export DEX_KUBECONFIG=$TEMPDIR/kubeconfig
|
|
||||||
go test -v -i ./storage/kubernetes
|
|
||||||
go test -v ./storage/kubernetes
|
|
|
@ -218,7 +218,6 @@ func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq)
|
||||||
return nil, fmt.Errorf("delete password: %v", err)
|
return nil, fmt.Errorf("delete password: %v", err)
|
||||||
}
|
}
|
||||||
return &api.DeletePasswordResp{}, nil
|
return &api.DeletePasswordResp{}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.VersionResp, error) {
|
func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.VersionResp, error) {
|
||||||
|
@ -248,7 +247,6 @@ func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*a
|
||||||
return &api.ListPasswordResp{
|
return &api.ListPasswordResp{
|
||||||
Passwords: passwords,
|
Passwords: passwords,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) (*api.VerifyPasswordResp, error) {
|
func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) (*api.VerifyPasswordResp, error) {
|
||||||
|
|
|
@ -167,7 +167,6 @@ func TestPassword(t *testing.T) {
|
||||||
if _, err := client.DeletePassword(ctx, &deleteReq); err != nil {
|
if _, err := client.DeletePassword(ctx, &deleteReq); err != nil {
|
||||||
t.Fatalf("Unable to delete password: %v", err)
|
t.Fatalf("Unable to delete password: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures checkCost returns expected values
|
// Ensures checkCost returns expected values
|
||||||
|
@ -495,7 +494,6 @@ func TestUpdateClient(t *testing.T) {
|
||||||
if tc.cleanup != nil {
|
if tc.cleanup != nil {
|
||||||
tc.cleanup(t, tc.req.Id)
|
tc.cleanup(t, tc.req.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,8 +45,8 @@ func (s *Server) newHealthChecker(ctx context.Context) http.Handler {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// healthChecker periodically performs health checks on server dependenices.
|
// healthChecker periodically performs health checks on server dependencies.
|
||||||
// Currently, it only checks that the storage layer is avialable.
|
// Currently, it only checks that the storage layer is available.
|
||||||
type healthChecker struct {
|
type healthChecker struct {
|
||||||
s *Server
|
s *Server
|
||||||
|
|
||||||
|
@ -259,16 +259,15 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectorInfos := make([]connectorInfo, len(connectors))
|
connectorInfos := make([]connectorInfo, len(connectors))
|
||||||
i := 0
|
for index, conn := range connectors {
|
||||||
for _, conn := range connectors {
|
connectorInfos[index] = connectorInfo{
|
||||||
connectorInfos[i] = connectorInfo{
|
|
||||||
ID: conn.ID,
|
ID: conn.ID,
|
||||||
Name: conn.Name,
|
Name: conn.Name,
|
||||||
|
Type: conn.Type,
|
||||||
// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
|
// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
|
||||||
// on create the auth request.
|
// on create the auth request.
|
||||||
URL: s.absPath("/auth", conn.ID) + "?req=" + authReq.ID,
|
URL: s.absPath("/auth", conn.ID) + "?req=" + authReq.ID,
|
||||||
}
|
}
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.templates.login(r, w, connectorInfos, r.URL.Path); err != nil {
|
if err := s.templates.login(r, w, connectorInfos, r.URL.Path); err != nil {
|
||||||
|
@ -922,7 +921,6 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
|
||||||
deleteToken = true
|
deleteToken = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.writeAccessToken(w, idToken, accessToken, refreshToken, expiry)
|
s.writeAccessToken(w, idToken, accessToken, refreshToken, expiry)
|
||||||
|
|
|
@ -24,7 +24,6 @@ func TestHandleHealth(t *testing.T) {
|
||||||
if rr.Code != http.StatusOK {
|
if rr.Code != http.StatusOK {
|
||||||
t.Errorf("expected 200 got %d", rr.Code)
|
t.Errorf("expected 200 got %d", rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type badStorage struct {
|
type badStorage struct {
|
||||||
|
|
|
@ -14,6 +14,10 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/felixge/httpsnoop"
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector"
|
"github.com/dexidp/dex/connector"
|
||||||
|
@ -28,13 +32,10 @@ import (
|
||||||
"github.com/dexidp/dex/connector/microsoft"
|
"github.com/dexidp/dex/connector/microsoft"
|
||||||
"github.com/dexidp/dex/connector/mock"
|
"github.com/dexidp/dex/connector/mock"
|
||||||
"github.com/dexidp/dex/connector/oidc"
|
"github.com/dexidp/dex/connector/oidc"
|
||||||
|
"github.com/dexidp/dex/connector/openshift"
|
||||||
"github.com/dexidp/dex/connector/saml"
|
"github.com/dexidp/dex/connector/saml"
|
||||||
"github.com/dexidp/dex/pkg/log"
|
"github.com/dexidp/dex/pkg/log"
|
||||||
"github.com/dexidp/dex/storage"
|
"github.com/dexidp/dex/storage"
|
||||||
"github.com/felixge/httpsnoop"
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocalConnector is the local passwordDB connector which is an internal
|
// LocalConnector is the local passwordDB connector which is an internal
|
||||||
|
@ -461,6 +462,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
|
||||||
"linkedin": func() ConnectorConfig { return new(linkedin.Config) },
|
"linkedin": func() ConnectorConfig { return new(linkedin.Config) },
|
||||||
"microsoft": func() ConnectorConfig { return new(microsoft.Config) },
|
"microsoft": func() ConnectorConfig { return new(microsoft.Config) },
|
||||||
"bitbucket-cloud": func() ConnectorConfig { return new(bitbucketcloud.Config) },
|
"bitbucket-cloud": func() ConnectorConfig { return new(bitbucketcloud.Config) },
|
||||||
|
"openshift": func() ConnectorConfig { return new(openshift.Config) },
|
||||||
// Keep around for backwards compatibility.
|
// Keep around for backwards compatibility.
|
||||||
"samlExperimental": func() ConnectorConfig { return new(saml.Config) },
|
"samlExperimental": func() ConnectorConfig { return new(saml.Config) },
|
||||||
}
|
}
|
||||||
|
|
|
@ -590,6 +590,8 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get failed: %v", err)
|
t.Fatalf("get failed: %v", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -726,6 +728,8 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get failed: %v", err)
|
t.Fatalf("get failed: %v", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -799,7 +803,6 @@ func TestCrossClientScopes(t *testing.T) {
|
||||||
if !reflect.DeepEqual(idToken.Audience, expAudience) {
|
if !reflect.DeepEqual(idToken.Audience, expAudience) {
|
||||||
t.Errorf("expected audience %q, got %q", expAudience, idToken.Audience)
|
t.Errorf("expected audience %q, got %q", expAudience, idToken.Audience)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if gotState := q.Get("state"); gotState != state {
|
if gotState := q.Get("state"); gotState != state {
|
||||||
t.Errorf("state did not match, want=%q got=%q", state, gotState)
|
t.Errorf("state did not match, want=%q got=%q", state, gotState)
|
||||||
|
@ -848,6 +851,8 @@ func TestCrossClientScopes(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get failed: %v", err)
|
t.Fatalf("get failed: %v", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -921,7 +926,6 @@ func TestCrossClientScopesWithAzpInAudienceByDefault(t *testing.T) {
|
||||||
if !reflect.DeepEqual(idToken.Audience, expAudience) {
|
if !reflect.DeepEqual(idToken.Audience, expAudience) {
|
||||||
t.Errorf("expected audience %q, got %q", expAudience, idToken.Audience)
|
t.Errorf("expected audience %q, got %q", expAudience, idToken.Audience)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if gotState := q.Get("state"); gotState != state {
|
if gotState := q.Get("state"); gotState != state {
|
||||||
t.Errorf("state did not match, want=%q got=%q", state, gotState)
|
t.Errorf("state did not match, want=%q got=%q", state, gotState)
|
||||||
|
@ -969,6 +973,8 @@ func TestCrossClientScopesWithAzpInAudienceByDefault(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get failed: %v", err)
|
t.Fatalf("get failed: %v", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -1058,7 +1064,6 @@ func TestPasswordDB(t *testing.T) {
|
||||||
t.Errorf("%s: %s", tc.name, diff)
|
t.Errorf("%s: %s", tc.name, diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPasswordDBUsernamePrompt(t *testing.T) {
|
func TestPasswordDBUsernamePrompt(t *testing.T) {
|
||||||
|
@ -1225,9 +1230,11 @@ func TestRefreshTokenFlow(t *testing.T) {
|
||||||
RedirectURL: redirectURL,
|
RedirectURL: redirectURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = http.Get(oauth2Client.server.URL + "/login"); err != nil {
|
resp, err := http.Get(oauth2Client.server.URL + "/login")
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("get failed: %v", err)
|
t.Fatalf("get failed: %v", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
tok := &oauth2.Token{
|
tok := &oauth2.Token{
|
||||||
RefreshToken: oauth2Client.token.RefreshToken,
|
RefreshToken: oauth2Client.token.RefreshToken,
|
||||||
|
@ -1235,9 +1242,11 @@ func TestRefreshTokenFlow(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login in again to receive a new token.
|
// Login in again to receive a new token.
|
||||||
if _, err = http.Get(oauth2Client.server.URL + "/login"); err != nil {
|
resp, err = http.Get(oauth2Client.server.URL + "/login")
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("get failed: %v", err)
|
t.Fatalf("get failed: %v", err)
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// try to refresh expired token with old refresh token.
|
// try to refresh expired token with old refresh token.
|
||||||
if _, err := oauth2Client.config.TokenSource(ctx, tok).Token(); err == nil {
|
if _, err := oauth2Client.config.TokenSource(ctx, tok).Token(); err == nil {
|
||||||
|
|
|
@ -47,19 +47,6 @@ type webConfig struct {
|
||||||
extra map[string]string
|
extra map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func join(base, path string) string {
|
|
||||||
b := strings.HasSuffix(base, "/")
|
|
||||||
p := strings.HasPrefix(path, "/")
|
|
||||||
switch {
|
|
||||||
case b && p:
|
|
||||||
return base + path[1:]
|
|
||||||
case b || p:
|
|
||||||
return base + path
|
|
||||||
default:
|
|
||||||
return base + "/" + path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dirExists(dir string) error {
|
func dirExists(dir string) error {
|
||||||
stat, err := os.Stat(dir)
|
stat, err := os.Stat(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -189,7 +176,6 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
|
||||||
//assetPath is static/main.css
|
//assetPath is static/main.css
|
||||||
//relativeURL("/dex", "/dex/auth", "static/main.css") = "../static/main.css"
|
//relativeURL("/dex", "/dex/auth", "static/main.css") = "../static/main.css"
|
||||||
func relativeURL(serverPath, reqPath, assetPath string) string {
|
func relativeURL(serverPath, reqPath, assetPath string) string {
|
||||||
|
|
||||||
splitPath := func(p string) []string {
|
splitPath := func(p string) []string {
|
||||||
res := []string{}
|
res := []string{}
|
||||||
parts := strings.Split(path.Clean(p), "/")
|
parts := strings.Split(path.Clean(p), "/")
|
||||||
|
@ -220,6 +206,7 @@ func relativeURL(serverPath, reqPath, assetPath string) string {
|
||||||
server, req, asset := splitPath(serverPath), splitPath(reqPath), splitPath(assetPath)
|
server, req, asset := splitPath(serverPath), splitPath(reqPath), splitPath(assetPath)
|
||||||
|
|
||||||
// Remove common prefix of request path with server path
|
// Remove common prefix of request path with server path
|
||||||
|
// nolint: ineffassign
|
||||||
server, req = stripCommonParts(server, req)
|
server, req = stripCommonParts(server, req)
|
||||||
|
|
||||||
// Remove common prefix of request path with asset path
|
// Remove common prefix of request path with asset path
|
||||||
|
@ -246,6 +233,7 @@ type connectorInfo struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
URL string
|
URL string
|
||||||
|
Type string
|
||||||
}
|
}
|
||||||
|
|
||||||
type byName []connectorInfo
|
type byName []connectorInfo
|
||||||
|
|
|
@ -161,6 +161,8 @@ func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
|
||||||
t.Fatalf("failed to delete auth request: %v", err)
|
t.Fatalf("failed to delete auth request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = s.GetAuthRequest(a1.ID)
|
||||||
|
mustBeErrNotFound(t, "auth request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
|
func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
|
||||||
|
@ -214,7 +216,7 @@ func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
|
||||||
|
|
||||||
got, err := s.GetAuthCode(a1.ID)
|
got, err := s.GetAuthCode(a1.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to get auth req: %v", err)
|
t.Fatalf("failed to get auth code: %v", err)
|
||||||
}
|
}
|
||||||
if a1.Expiry.Unix() != got.Expiry.Unix() {
|
if a1.Expiry.Unix() != got.Expiry.Unix() {
|
||||||
t.Errorf("auth code expiry did not match want=%s vs got=%s", a1.Expiry, got.Expiry)
|
t.Errorf("auth code expiry did not match want=%s vs got=%s", a1.Expiry, got.Expiry)
|
||||||
|
@ -509,7 +511,6 @@ func testPasswordCRUD(t *testing.T, s storage.Storage) {
|
||||||
|
|
||||||
_, err = s.GetPassword(password1.Email)
|
_, err = s.GetPassword(password1.Email)
|
||||||
mustBeErrNotFound(t, "password", err)
|
mustBeErrNotFound(t, "password", err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOfflineSessionCRUD(t *testing.T, s storage.Storage) {
|
func testOfflineSessionCRUD(t *testing.T, s storage.Storage) {
|
||||||
|
@ -814,7 +815,7 @@ func testGC(t *testing.T, s storage.Storage) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := s.GetAuthRequest(a.ID); err != nil {
|
if _, err := s.GetAuthRequest(a.ID); err != nil {
|
||||||
t.Errorf("expected to be able to get auth code after GC: %v", err)
|
t.Errorf("expected to be able to get auth request after GC: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -825,7 +826,7 @@ func testGC(t *testing.T, s storage.Storage) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.GetAuthRequest(a.ID); err == nil {
|
if _, err := s.GetAuthRequest(a.ID); err == nil {
|
||||||
t.Errorf("expected auth code to be GC'd")
|
t.Errorf("expected auth request to be GC'd")
|
||||||
} else if err != storage.ErrNotFound {
|
} else if err != storage.ErrNotFound {
|
||||||
t.Errorf("expected storage.ErrNotFound, got %v", err)
|
t.Errorf("expected storage.ErrNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,8 +135,8 @@ func testKeysConcurrentUpdate(t *testing.T, s storage.Storage) {
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
n := time.Now().UTC().Round(time.Second)
|
n := time.Now().UTC().Round(time.Second)
|
||||||
keys1 := storage.Keys{
|
keys1 := storage.Keys{
|
||||||
SigningKey: jsonWebKeys[0].Private,
|
SigningKey: jsonWebKeys[i].Private,
|
||||||
SigningKeyPub: jsonWebKeys[0].Public,
|
SigningKeyPub: jsonWebKeys[i].Public,
|
||||||
NextRotation: n,
|
NextRotation: n,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ func (c *conn) UpdateRefreshToken(id string, updater func(old storage.RefreshTok
|
||||||
return c.txnUpdate(ctx, keyID(refreshTokenPrefix, id), func(currentValue []byte) ([]byte, error) {
|
return c.txnUpdate(ctx, keyID(refreshTokenPrefix, id), func(currentValue []byte) ([]byte, error) {
|
||||||
var current RefreshToken
|
var current RefreshToken
|
||||||
if len(currentValue) > 0 {
|
if len(currentValue) > 0 {
|
||||||
if err := json.Unmarshal([]byte(currentValue), ¤t); err != nil {
|
if err := json.Unmarshal(currentValue, ¤t); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,14 +55,14 @@ type client struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// idToName maps an arbitrary ID, such as an email or client ID to a Kubernetes object name.
|
// idToName maps an arbitrary ID, such as an email or client ID to a Kubernetes object name.
|
||||||
func (c *client) idToName(s string) string {
|
func (cli *client) idToName(s string) string {
|
||||||
return idToName(s, c.hash)
|
return idToName(s, cli.hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// offlineTokenName maps two arbitrary IDs, to a single Kubernetes object name.
|
// offlineTokenName maps two arbitrary IDs, to a single Kubernetes object name.
|
||||||
// This is used when more than one field is used to uniquely identify the object.
|
// This is used when more than one field is used to uniquely identify the object.
|
||||||
func (c *client) offlineTokenName(userID string, connID string) string {
|
func (cli *client) offlineTokenName(userID string, connID string) string {
|
||||||
return offlineTokenName(userID, connID, c.hash)
|
return offlineTokenName(userID, connID, cli.hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kubernetes names must match the regexp '[a-z0-9]([-a-z0-9]*[a-z0-9])?'.
|
// Kubernetes names must match the regexp '[a-z0-9]([-a-z0-9]*[a-z0-9])?'.
|
||||||
|
@ -79,7 +79,7 @@ func offlineTokenName(userID string, connID string, h func() hash.Hash) string {
|
||||||
return strings.TrimRight(encoding.EncodeToString(hash.Sum(nil)), "=")
|
return strings.TrimRight(encoding.EncodeToString(hash.Sum(nil)), "=")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
|
func (cli *client) urlFor(apiVersion, namespace, resource, name string) string {
|
||||||
basePath := "apis/"
|
basePath := "apis/"
|
||||||
if apiVersion == "v1" {
|
if apiVersion == "v1" {
|
||||||
basePath = "api/"
|
basePath = "api/"
|
||||||
|
@ -91,10 +91,10 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
|
||||||
} else {
|
} else {
|
||||||
p = path.Join(basePath, apiVersion, resource, name)
|
p = path.Join(basePath, apiVersion, resource, name)
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(c.baseURL, "/") {
|
if strings.HasSuffix(cli.baseURL, "/") {
|
||||||
return c.baseURL + p
|
return cli.baseURL + p
|
||||||
}
|
}
|
||||||
return c.baseURL + "/" + p
|
return cli.baseURL + "/" + p
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define an error interface so we can get at the underlying status code if it's
|
// Define an error interface so we can get at the underlying status code if it's
|
||||||
|
@ -156,13 +156,13 @@ func closeResp(r *http.Response) {
|
||||||
r.Body.Close()
|
r.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) get(resource, name string, v interface{}) error {
|
func (cli *client) get(resource, name string, v interface{}) error {
|
||||||
return c.getResource(c.apiVersion, c.namespace, resource, name, v)
|
return cli.getResource(cli.apiVersion, cli.namespace, resource, name, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) getResource(apiVersion, namespace, resource, name string, v interface{}) error {
|
func (cli *client) getResource(apiVersion, namespace, resource, name string, v interface{}) error {
|
||||||
url := c.urlFor(apiVersion, namespace, resource, name)
|
url := cli.urlFor(apiVersion, namespace, resource, name)
|
||||||
resp, err := c.client.Get(url)
|
resp, err := cli.client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -173,22 +173,22 @@ func (c *client) getResource(apiVersion, namespace, resource, name string, v int
|
||||||
return json.NewDecoder(resp.Body).Decode(v)
|
return json.NewDecoder(resp.Body).Decode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) list(resource string, v interface{}) error {
|
func (cli *client) list(resource string, v interface{}) error {
|
||||||
return c.get(resource, "", v)
|
return cli.get(resource, "", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) post(resource string, v interface{}) error {
|
func (cli *client) post(resource string, v interface{}) error {
|
||||||
return c.postResource(c.apiVersion, c.namespace, resource, v)
|
return cli.postResource(cli.apiVersion, cli.namespace, resource, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) postResource(apiVersion, namespace, resource string, v interface{}) error {
|
func (cli *client) postResource(apiVersion, namespace, resource string, v interface{}) error {
|
||||||
body, err := json.Marshal(v)
|
body, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal object: %v", err)
|
return fmt.Errorf("marshal object: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
url := c.urlFor(apiVersion, namespace, resource, "")
|
url := cli.urlFor(apiVersion, namespace, resource, "")
|
||||||
resp, err := c.client.Post(url, "application/json", bytes.NewReader(body))
|
resp, err := cli.client.Post(url, "application/json", bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -196,13 +196,13 @@ func (c *client) postResource(apiVersion, namespace, resource string, v interfac
|
||||||
return checkHTTPErr(resp, http.StatusCreated)
|
return checkHTTPErr(resp, http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) delete(resource, name string) error {
|
func (cli *client) delete(resource, name string) error {
|
||||||
url := c.urlFor(c.apiVersion, c.namespace, resource, name)
|
url := cli.urlFor(cli.apiVersion, cli.namespace, resource, name)
|
||||||
req, err := http.NewRequest("DELETE", url, nil)
|
req, err := http.NewRequest("DELETE", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create delete request: %v", err)
|
return fmt.Errorf("create delete request: %v", err)
|
||||||
}
|
}
|
||||||
resp, err := c.client.Do(req)
|
resp, err := cli.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delete request: %v", err)
|
return fmt.Errorf("delete request: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -210,7 +210,7 @@ func (c *client) delete(resource, name string) error {
|
||||||
return checkHTTPErr(resp, http.StatusOK)
|
return checkHTTPErr(resp, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) deleteAll(resource string) error {
|
func (cli *client) deleteAll(resource string) error {
|
||||||
var list struct {
|
var list struct {
|
||||||
k8sapi.TypeMeta `json:",inline"`
|
k8sapi.TypeMeta `json:",inline"`
|
||||||
k8sapi.ListMeta `json:"metadata,omitempty"`
|
k8sapi.ListMeta `json:"metadata,omitempty"`
|
||||||
|
@ -219,24 +219,24 @@ func (c *client) deleteAll(resource string) error {
|
||||||
k8sapi.ObjectMeta `json:"metadata,omitempty"`
|
k8sapi.ObjectMeta `json:"metadata,omitempty"`
|
||||||
} `json:"items"`
|
} `json:"items"`
|
||||||
}
|
}
|
||||||
if err := c.list(resource, &list); err != nil {
|
if err := cli.list(resource, &list); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, item := range list.Items {
|
for _, item := range list.Items {
|
||||||
if err := c.delete(resource, item.Name); err != nil {
|
if err := cli.delete(resource, item.Name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) put(resource, name string, v interface{}) error {
|
func (cli *client) put(resource, name string, v interface{}) error {
|
||||||
body, err := json.Marshal(v)
|
body, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal object: %v", err)
|
return fmt.Errorf("marshal object: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
url := c.urlFor(c.apiVersion, c.namespace, resource, name)
|
url := cli.urlFor(cli.apiVersion, cli.namespace, resource, name)
|
||||||
req, err := http.NewRequest("PUT", url, bytes.NewReader(body))
|
req, err := http.NewRequest("PUT", url, bytes.NewReader(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create patch request: %v", err)
|
return fmt.Errorf("create patch request: %v", err)
|
||||||
|
@ -244,7 +244,7 @@ func (c *client) put(resource, name string, v interface{}) error {
|
||||||
|
|
||||||
req.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
req.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
resp, err := cli.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("patch request: %v", err)
|
return fmt.Errorf("patch request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ type CustomResourceDefinitionNames struct {
|
||||||
ListKind string `json:"listKind,omitempty" protobuf:"bytes,5,opt,name=listKind"`
|
ListKind string `json:"listKind,omitempty" protobuf:"bytes,5,opt,name=listKind"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceScope is an enum defining the different scopes availabe to a custom resource
|
// ResourceScope is an enum defining the different scopes available to a custom resource
|
||||||
type ResourceScope string
|
type ResourceScope string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,39 +1,104 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"sigs.k8s.io/testing_frameworks/integration"
|
||||||
|
|
||||||
"github.com/dexidp/dex/storage"
|
"github.com/dexidp/dex/storage"
|
||||||
"github.com/dexidp/dex/storage/conformance"
|
"github.com/dexidp/dex/storage/conformance"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testKubeConfigEnv = "DEX_KUBECONFIG"
|
const kubeconfigTemplate = `apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
clusters:
|
||||||
|
- name: local
|
||||||
|
cluster:
|
||||||
|
server: SERVERURL
|
||||||
|
users:
|
||||||
|
- name: local
|
||||||
|
user:
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: local
|
||||||
|
user: local
|
||||||
|
`
|
||||||
|
|
||||||
func TestLoadClient(t *testing.T) {
|
func TestStorage(t *testing.T) {
|
||||||
loadClient(t)
|
if os.Getenv("TEST_ASSET_KUBE_APISERVER") == "" || os.Getenv("TEST_ASSET_ETCD") == "" {
|
||||||
|
t.Skip("control plane binaries are missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadClient(t *testing.T) *client {
|
suite.Run(t, new(StorageTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
controlPlane *integration.ControlPlane
|
||||||
|
|
||||||
|
client *client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageTestSuite) SetupSuite() {
|
||||||
|
s.controlPlane = &integration.ControlPlane{}
|
||||||
|
|
||||||
|
err := s.controlPlane.Start()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageTestSuite) TearDownSuite() {
|
||||||
|
s.controlPlane.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageTestSuite) SetupTest() {
|
||||||
|
f, err := ioutil.TempFile("", "dex-kubeconfig-*")
|
||||||
|
s.Require().NoError(err)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = f.WriteString(strings.ReplaceAll(kubeconfigTemplate, "SERVERURL", s.controlPlane.APIURL().String()))
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
config := Config{
|
config := Config{
|
||||||
KubeConfigFile: os.Getenv(testKubeConfigEnv),
|
KubeConfigFile: f.Name(),
|
||||||
}
|
|
||||||
if config.KubeConfigFile == "" {
|
|
||||||
t.Skipf("test environment variable %q not set, skipping", testKubeConfigEnv)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := &logrus.Logger{
|
logger := &logrus.Logger{
|
||||||
Out: os.Stderr,
|
Out: os.Stderr,
|
||||||
Formatter: &logrus.TextFormatter{DisableColors: true},
|
Formatter: &logrus.TextFormatter{DisableColors: true},
|
||||||
Level: logrus.DebugLevel,
|
Level: logrus.DebugLevel,
|
||||||
}
|
}
|
||||||
s, err := config.open(logger, true)
|
|
||||||
if err != nil {
|
client, err := config.open(logger, true)
|
||||||
t.Fatal(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
s.client = client
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
|
func (s *StorageTestSuite) TestStorage() {
|
||||||
|
newStorage := func() storage.Storage {
|
||||||
|
for _, resource := range []string{
|
||||||
|
resourceAuthCode,
|
||||||
|
resourceAuthRequest,
|
||||||
|
resourceClient,
|
||||||
|
resourceRefreshToken,
|
||||||
|
resourceKeys,
|
||||||
|
resourcePassword,
|
||||||
|
} {
|
||||||
|
if err := s.client.deleteAll(resource); err != nil {
|
||||||
|
s.T().Fatalf("delete all %q failed: %v", resource, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.client
|
||||||
|
}
|
||||||
|
|
||||||
|
conformance.RunTests(s.T(), newStorage)
|
||||||
|
conformance.RunTransactionTests(s.T(), newStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestURLFor(t *testing.T) {
|
func TestURLFor(t *testing.T) {
|
||||||
|
@ -83,27 +148,3 @@ func TestURLFor(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStorage(t *testing.T) {
|
|
||||||
client := loadClient(t)
|
|
||||||
newStorage := func() storage.Storage {
|
|
||||||
for _, resource := range []string{
|
|
||||||
resourceAuthCode,
|
|
||||||
resourceAuthRequest,
|
|
||||||
resourceClient,
|
|
||||||
resourceRefreshToken,
|
|
||||||
resourceKeys,
|
|
||||||
resourcePassword,
|
|
||||||
} {
|
|
||||||
if err := client.deleteAll(resource); err != nil {
|
|
||||||
// Fatalf sometimes doesn't print the error message.
|
|
||||||
fmt.Fprintf(os.Stderr, "delete all %q failed: %v\n", resource, err)
|
|
||||||
t.Fatalf("delete all %q failed: %v", resource, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
conformance.RunTests(t, newStorage)
|
|
||||||
conformance.RunTransactionTests(t, newStorage)
|
|
||||||
}
|
|
||||||
|
|
|
@ -308,10 +308,17 @@ func (s *MySQL) open(logger log.Logger) (*conn, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.MaxIdleConns == 0 {
|
||||||
|
/*Override default behaviour to fix https://github.com/dexidp/dex/issues/1608*/
|
||||||
|
db.SetMaxIdleConns(0)
|
||||||
|
} else {
|
||||||
|
db.SetMaxIdleConns(s.MaxIdleConns)
|
||||||
|
}
|
||||||
|
|
||||||
err = db.Ping()
|
err = db.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == mysqlErrUnknownSysVar {
|
if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == mysqlErrUnknownSysVar {
|
||||||
logger.Info("reconnecting with MySQL pre-5.7.20 compatibilty mode")
|
logger.Info("reconnecting with MySQL pre-5.7.20 compatibility mode")
|
||||||
|
|
||||||
// MySQL 5.7.20 introduced transaction_isolation and deprecated tx_isolation.
|
// MySQL 5.7.20 introduced transaction_isolation and deprecated tx_isolation.
|
||||||
// MySQL 8.0 doesn't have tx_isolation at all.
|
// MySQL 8.0 doesn't have tx_isolation at all.
|
||||||
|
|
|
@ -169,7 +169,6 @@ func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) GetAuthRequest(id string) (storage.AuthRequest, error) {
|
func (c *conn) GetAuthRequest(id string) (storage.AuthRequest, error) {
|
||||||
|
|
|
@ -68,12 +68,12 @@ body {
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dex-btn-icon--bitbucket {
|
.dex-btn-icon--bitbucket-cloud {
|
||||||
background-color: #205081;
|
background-color: #205081;
|
||||||
background-image: url(../static/img/bitbucket-icon.svg);
|
background-image: url(../static/img/bitbucket-icon.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dex-btn-icon--ldap, .dex-btn-icon--tectonic-ldap {
|
.dex-btn-icon--ldap {
|
||||||
background-color: #84B6EF;
|
background-color: #84B6EF;
|
||||||
background-image: url(../static/img/ldap-icon.svg);
|
background-image: url(../static/img/ldap-icon.svg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="theme-form-row">
|
<div class="theme-form-row">
|
||||||
<a href="{{ $c.URL }}" target="_self">
|
<a href="{{ $c.URL }}" target="_self">
|
||||||
<button class="dex-btn theme-btn-provider">
|
<button class="dex-btn theme-btn-provider">
|
||||||
<span class="dex-btn-icon dex-btn-icon--{{ $c.ID }}"></span>
|
<span class="dex-btn-icon dex-btn-icon--{{ $c.Type }}"></span>
|
||||||
<span class="dex-btn-text">Log in with {{ $c.Name }}</span>
|
<span class="dex-btn-text">Log in with {{ $c.Name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
Loading…
Reference in a new issue