Merge branch 'master' into feature/static_password_env

This commit is contained in:
Krishna Durai 2020-01-06 23:21:20 -08:00 committed by GitHub
commit 9560899496
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 897 additions and 279 deletions

View file

@ -69,8 +69,8 @@ jobs:
DEX_KEYSTONE_ADMIN_USER: demo
DEX_KEYSTONE_ADMIN_PASS: DEMO_PASS
- name: Run Kubernetes tests
run: ./scripts/test-k8s.sh
- name: Run linter
run: make lint
# Ensure proto generation doesn't depend on external packages.
- name: Verify proto

35
.golangci.yml Normal file
View 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

View file

@ -48,7 +48,6 @@ install:
script:
- make testall
- ./scripts/test-k8s.sh
- make verify-proto # Ensure proto generation doesn't depend on external packages.
notifications:

View 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
```

View file

@ -1,21 +1,5 @@
# 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
Running database tests locally requires:

View file

@ -14,11 +14,13 @@ Steps:
```yaml
frontend:
dir: /path/to/custom/web
issuer: my-dex
extra:
tos_footer_link: "https://example.com/terms"
client_logo_url: "../theme/client-logo.png"
foo: "bar"
```
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.

View file

@ -2,7 +2,6 @@ PROJ=dex
ORG_PATH=github.com/dexidp
REPO_PATH=$(ORG_PATH)/$(PROJ)
export PATH := $(PWD)/bin:$(PATH)
THIS_DIRECTORY:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell ./scripts/git-version)
@ -18,6 +17,9 @@ export GOBIN=$(PWD)/bin
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
bin/dex:
@ -39,20 +41,38 @@ revendor:
@go mod vendor -v
@go mod verify
test:
test: bin/test/kube-apiserver bin/test/etcd
@go test -v ./...
testrace:
testrace: bin/test/kube-apiserver bin/test/etcd
@go test -v --race ./...
vet:
@go vet ./...
export TEST_ASSET_KUBE_APISERVER=$(abspath bin/test/kube-apiserver)
export TEST_ASSET_ETCD=$(abspath bin/test/etcd)
fmt:
@./scripts/gofmt ./...
bin/test/kube-apiserver:
@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/golint -set_exit_status $(shell go list ./...)
bin/test/etcd:
@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
docker-image:
@ -73,14 +93,11 @@ bin/protoc: scripts/get-protoc
bin/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:
@rm -rf bin/
testall: testrace vet fmt lint
testall: testrace
FORCE:
.PHONY: test testrace vet fmt lint testall
.PHONY: test testrace testall

View file

@ -74,6 +74,7 @@ Dex implements the following connectors:
| [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. |
| [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:

View file

@ -11,6 +11,7 @@ import (
"github.com/dexidp/dex/connector/mock"
"github.com/dexidp/dex/connector/oidc"
"github.com/dexidp/dex/server"
"github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/sql"
)
@ -215,7 +216,6 @@ logger:
if diff := pretty.Compare(c, want); diff != "" {
t.Errorf("got!=want: %s", diff)
}
}
func TestUnmarshalConfigWithEnv(t *testing.T) {

View file

@ -182,7 +182,6 @@ func serve(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to initialize storage connectors: %v", err)
}
storageConnectors[i] = conn
}
if c.EnablePasswordDB {

View file

@ -143,7 +143,7 @@ func cmd() *cobra.Command {
ctx := oidc.ClientContext(context.Background(), a.client)
provider, err := oidc.NewProvider(ctx, issuerURL)
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 {
@ -153,7 +153,7 @@ func cmd() *cobra.Command {
ScopesSupported []string `json:"scopes_supported"`
}
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 {

View file

@ -41,7 +41,6 @@ type Config struct {
// Open returns a strategy for logging in through Bitbucket.
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
b := bitbucketConnector{
redirectURI: c.RedirectURI,
teams: c.Teams,
@ -373,7 +372,6 @@ type userTeamsResponse struct {
}
func (b *bitbucketConnector) userTeams(ctx context.Context, client *http.Client) ([]string, error) {
var teams []string
apiURL := b.apiURL + "/teams?role=member"

View file

@ -14,7 +14,6 @@ import (
)
func TestUserGroups(t *testing.T) {
teamsResponse := userTeamsResponse{
pagedResponse: pagedResponse{
Size: 3,
@ -46,7 +45,6 @@ func TestUserGroups(t *testing.T) {
}
func TestUserWithoutTeams(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/teams?role=member": userTeamsResponse{},
})
@ -61,7 +59,6 @@ func TestUserWithoutTeams(t *testing.T) {
}
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/user": user{Username: "some-login"},
"/user/emails": userEmailResponse{

View file

@ -67,7 +67,6 @@ type Org struct {
// Open returns a strategy for logging in through GitHub.
func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
if c.Org != "" {
// Return error if both 'org' and 'orgs' fields are used.
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 {
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
}
}
g.loadAllGroups = c.LoadAllGroups
@ -144,7 +142,7 @@ type githubConnector struct {
hostName string
// Used to support untrusted/self-signed CA certs.
rootCA string
// HTTP Client that trusts the custom delcared rootCA cert.
// HTTP Client that trusts the custom declared rootCA cert.
httpClient *http.Client
// optional choice between 'name' (default) or 'slug'
teamNameField string
@ -206,7 +204,7 @@ func (e *oauth2Error) Error() string {
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) {
tlsConfig := tls.Config{RootCAs: x509.NewCertPool()}
rootCABytes, err := ioutil.ReadFile(rootCA)

View file

@ -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
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
s := newTestServer(map[string]testResponse{
"/user": {data: user{Login: "some-login", ID: 12345678}},
"/user/emails": {data: []userEmail{{
@ -168,7 +167,6 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
}
func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
s := newTestServer(map[string]testResponse{
"/user": {data: user{Login: "some-login", ID: 12345678, Name: "Joe Bloggs"}},
"/user/emails": {data: []userEmail{{

View file

@ -65,7 +65,6 @@ func TestUserGroupsWithoutOrgs(t *testing.T) {
// tests that the email is used as their username when they have no username set
func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678},
"/oauth/token": map[string]interface{}{
@ -102,7 +101,6 @@ func TestUsernameIncludedInFederatedIdentity(t *testing.T) {
}
func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"},
"/oauth/token": map[string]interface{}{
@ -130,7 +128,6 @@ func TestLoginUsedAsIDWhenConfigured(t *testing.T) {
}
func TestLoginWithTeamWhitelisted(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs"},
"/oauth/token": map[string]interface{}{
@ -158,7 +155,6 @@ func TestLoginWithTeamWhitelisted(t *testing.T) {
}
func TestLoginWithTeamNonWhitelisted(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/api/v4/user": gitlabUser{Email: "some@email.com", ID: 12345678, Name: "Joe Bloggs", Username: "joebloggs"},
"/oauth/token": map[string]interface{}{

View file

@ -11,12 +11,12 @@ import (
"github.com/coreos/go-oidc"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
admin "google.golang.org/api/admin/directory/v1"
"github.com/dexidp/dex/connector"
pkg_groups "github.com/dexidp/dex/pkg/groups"
"github.com/dexidp/dex/pkg/log"
"golang.org/x/oauth2/google"
admin "google.golang.org/api/admin/directory/v1"
)
const (
@ -105,7 +105,6 @@ type googleConnector struct {
redirectURI string
oauth2Config *oauth2.Config
verifier *oidc.IDTokenVerifier
ctx context.Context
cancel context.CancelFunc
logger log.Logger
hostedDomains []string

View file

@ -150,7 +150,6 @@ func (p *conn) Prompt() string { return "username" }
func (p *conn) Refresh(
ctx context.Context, scopes connector.Scopes, identity connector.Identity) (connector.Identity, error) {
token, err := p.getAdminToken(ctx)
if err != nil {
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 {
return "", err
}
defer resp.Body.Close()
token := resp.Header.Get("X-Subject-Token")
return token, nil
}
@ -229,6 +230,7 @@ func (p *conn) checkIfUserExists(ctx context.Context, userID string, token strin
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return true, nil

View file

@ -154,7 +154,12 @@ func delete(t *testing.T, token, id, uri string) {
t.Fatalf("error: %v", err)
}
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 {
@ -208,7 +213,13 @@ func addUserToGroup(t *testing.T, token, groupID, userID string) error {
return err
}
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
}
@ -274,7 +285,7 @@ func TestUseRefreshToken(t *testing.T) {
delete(t, token, groupID, groupsURL)
expectEquals(t, 1, len(identityRefresh.Groups))
expectEquals(t, testGroup, string(identityRefresh.Groups[0]))
expectEquals(t, testGroup, identityRefresh.Groups[0])
}
func TestUseRefreshTokenUserDeleted(t *testing.T) {

View file

@ -189,7 +189,6 @@ func (c *Config) OpenConnector(logger log.Logger) (interface {
}
func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) {
requiredFields := []struct {
name 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) {
filter := fmt.Sprintf("(%s=%s)", c.UserSearch.Username, ldap.EscapeFilter(username))
if c.UserSearch.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", c.UserSearch.Filter, filter)

View file

@ -9,7 +9,6 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/coreos/go-oidc"
@ -85,18 +84,6 @@ func knownBrokenAuthHeaderProvider(issuerURL string) bool {
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
// OpenID Connect provider.
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)
}
endpoint := provider.Endpoint()
if c.BasicAuthUnsupported != nil {
// Setting "basicAuthUnsupported" always overrides our detection.
if *c.BasicAuthUnsupported {
registerBrokenAuthHeaderProvider(provider.Endpoint().TokenURL)
endpoint.AuthStyle = oauth2.AuthStyleInParams
}
} else if knownBrokenAuthHeaderProvider(c.Issuer) {
registerBrokenAuthHeaderProvider(provider.Endpoint().TokenURL)
endpoint.AuthStyle = oauth2.AuthStyleInParams
}
scopes := []string{oidc.ScopeOpenID}
@ -131,7 +120,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
oauth2Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: c.ClientSecret,
Endpoint: provider.Endpoint(),
Endpoint: endpoint,
Scopes: scopes,
RedirectURL: c.RedirectURI,
},
@ -273,15 +262,25 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
if !found {
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)
if !found {
if !found && hasEmailScope {
return identity, errors.New("missing \"email\" claim")
}
emailVerified, found := claims["email_verified"].(bool)
if !found {
if c.insecureSkipEmailVerified {
emailVerified = true
} else {
} else if hasEmailScope {
return identity, errors.New("missing \"email_verified\" claim")
}
}

View file

@ -16,9 +16,10 @@ import (
"testing"
"time"
"github.com/dexidp/dex/connector"
"github.com/sirupsen/logrus"
"gopkg.in/square/go-jose.v2"
"github.com/dexidp/dex/connector"
)
func TestKnownBrokenAuthHeaderProvider(t *testing.T) {
@ -49,8 +50,10 @@ func TestHandleCallback(t *testing.T) {
userIDKey string
userNameKey string
insecureSkipEmailVerified bool
scopes []string
expectUserID string
expectUserName string
expectedEmailField string
token map[string]interface{}
}{
{
@ -59,6 +62,7 @@ func TestHandleCallback(t *testing.T) {
userNameKey: "", // not configured
expectUserID: "subvalue",
expectUserName: "namevalue",
expectedEmailField: "emailvalue",
token: map[string]interface{}{
"sub": "subvalue",
"name": "namevalue",
@ -71,6 +75,7 @@ func TestHandleCallback(t *testing.T) {
insecureSkipEmailVerified: true,
expectUserID: "subvalue",
expectUserName: "namevalue",
expectedEmailField: "emailvalue",
token: map[string]interface{}{
"sub": "subvalue",
"name": "namevalue",
@ -82,6 +87,7 @@ func TestHandleCallback(t *testing.T) {
userIDKey: "name",
expectUserID: "namevalue",
expectUserName: "namevalue",
expectedEmailField: "emailvalue",
token: map[string]interface{}{
"sub": "subvalue",
"name": "namevalue",
@ -94,6 +100,7 @@ func TestHandleCallback(t *testing.T) {
userNameKey: "user_name",
expectUserID: "subvalue",
expectUserName: "username",
expectedEmailField: "emailvalue",
token: map[string]interface{}{
"sub": "subvalue",
"user_name": "username",
@ -101,6 +108,33 @@ func TestHandleCallback(t *testing.T) {
"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 {
@ -110,16 +144,25 @@ func TestHandleCallback(t *testing.T) {
t.Fatal("failed to setup test server", err)
}
defer testServer.Close()
var scopes []string
if len(tc.scopes) > 0 {
scopes = tc.scopes
} else {
scopes = []string{"email", "groups"}
}
serverURL := testServer.URL
basicAuth := true
config := Config{
Issuer: serverURL,
ClientID: "clientID",
ClientSecret: "clientSecret",
Scopes: []string{"groups"},
Scopes: scopes,
RedirectURI: fmt.Sprintf("%s/callback", serverURL),
UserIDKey: tc.userIDKey,
UserNameKey: tc.userNameKey,
InsecureSkipEmailVerified: tc.insecureSkipEmailVerified,
BasicAuthUnsupported: &basicAuth,
}
conn, err := newConnector(config)
@ -139,7 +182,7 @@ func TestHandleCallback(t *testing.T) {
expectEquals(t, identity.UserID, tc.expectUserID)
expectEquals(t, identity.Username, tc.expectUserName)
expectEquals(t, identity.Email, "emailvalue")
expectEquals(t, identity.Email, tc.expectedEmailField)
expectEquals(t, identity.EmailVerified, true)
})
}

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

View 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)
}
}

View file

@ -14,11 +14,12 @@ import (
"time"
"github.com/beevik/etree"
dsig "github.com/russellhaering/goxmldsig"
"github.com/russellhaering/goxmldsig/etreeutils"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/pkg/groups"
"github.com/dexidp/dex/pkg/log"
dsig "github.com/russellhaering/goxmldsig"
"github.com/russellhaering/goxmldsig/etreeutils"
)
// nolint
@ -248,7 +249,6 @@ type provider struct {
}
func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) {
r := &authnRequest{
ProtocolBinding: bindingPOST,
ID: id,
@ -325,7 +325,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
// Status is a required element.
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 {
@ -398,7 +398,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
if len(p.allowedGroups) > 0 && (!s.Groups || p.groupsAttr == "") {
// 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.
@ -427,7 +427,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
if len(groupMatches) == 0 {
// 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
@ -468,7 +468,7 @@ func (p *provider) validateStatus(status *status) error {
func (p *provider) validateSubject(subject *subject, inResponseTo string) error {
// Optional according to the spec, but again, we're going to be strict here.
if len(subject.SubjectConfirmations) == 0 {
return fmt.Errorf("Subject contained no SubjectConfirmations")
return fmt.Errorf("subject contained no SubjectConfirmations")
}
var errs []error

1
go.mod
View file

@ -57,6 +57,7 @@ require (
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
gopkg.in/ldap.v2 v2.5.1
gopkg.in/square/go-jose.v2 v2.3.1
sigs.k8s.io/testing_frameworks v0.1.2
)
go 1.13

13
go.sum
View file

@ -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/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/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.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
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.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
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/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
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-20180826012351-8a410e7b638d/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-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-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-20180905080454-ebe1bf3edb33/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
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 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-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/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
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/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/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
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/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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
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=

View file

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

View file

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

View file

@ -218,7 +218,6 @@ func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq)
return nil, fmt.Errorf("delete password: %v", err)
}
return &api.DeletePasswordResp{}, nil
}
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{
Passwords: passwords,
}, nil
}
func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) (*api.VerifyPasswordResp, error) {

View file

@ -167,7 +167,6 @@ func TestPassword(t *testing.T) {
if _, err := client.DeletePassword(ctx, &deleteReq); err != nil {
t.Fatalf("Unable to delete password: %v", err)
}
}
// Ensures checkCost returns expected values
@ -495,7 +494,6 @@ func TestUpdateClient(t *testing.T) {
if tc.cleanup != nil {
tc.cleanup(t, tc.req.Id)
}
})
}
}

View file

@ -45,8 +45,8 @@ func (s *Server) newHealthChecker(ctx context.Context) http.Handler {
return h
}
// healthChecker periodically performs health checks on server dependenices.
// Currently, it only checks that the storage layer is avialable.
// healthChecker periodically performs health checks on server dependencies.
// Currently, it only checks that the storage layer is available.
type healthChecker struct {
s *Server
@ -259,16 +259,15 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
}
connectorInfos := make([]connectorInfo, len(connectors))
i := 0
for _, conn := range connectors {
connectorInfos[i] = connectorInfo{
for index, conn := range connectors {
connectorInfos[index] = connectorInfo{
ID: conn.ID,
Name: conn.Name,
Type: conn.Type,
// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
// on create the auth request.
URL: s.absPath("/auth", conn.ID) + "?req=" + authReq.ID,
}
i++
}
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
return
}
}
}
s.writeAccessToken(w, idToken, accessToken, refreshToken, expiry)

View file

@ -24,7 +24,6 @@ func TestHandleHealth(t *testing.T) {
if rr.Code != http.StatusOK {
t.Errorf("expected 200 got %d", rr.Code)
}
}
type badStorage struct {

View file

@ -14,6 +14,10 @@ import (
"sync/atomic"
"time"
"github.com/felixge/httpsnoop"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/bcrypt"
"github.com/dexidp/dex/connector"
@ -28,13 +32,10 @@ import (
"github.com/dexidp/dex/connector/microsoft"
"github.com/dexidp/dex/connector/mock"
"github.com/dexidp/dex/connector/oidc"
"github.com/dexidp/dex/connector/openshift"
"github.com/dexidp/dex/connector/saml"
"github.com/dexidp/dex/pkg/log"
"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
@ -461,6 +462,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
"linkedin": func() ConnectorConfig { return new(linkedin.Config) },
"microsoft": func() ConnectorConfig { return new(microsoft.Config) },
"bitbucket-cloud": func() ConnectorConfig { return new(bitbucketcloud.Config) },
"openshift": func() ConnectorConfig { return new(openshift.Config) },
// Keep around for backwards compatibility.
"samlExperimental": func() ConnectorConfig { return new(saml.Config) },
}

View file

@ -590,6 +590,8 @@ func TestOAuth2CodeFlow(t *testing.T) {
if err != nil {
t.Fatalf("get failed: %v", err)
}
defer resp.Body.Close()
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
t.Fatal(err)
}
@ -726,6 +728,8 @@ func TestOAuth2ImplicitFlow(t *testing.T) {
if err != nil {
t.Fatalf("get failed: %v", err)
}
defer resp.Body.Close()
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
t.Fatal(err)
}
@ -799,7 +803,6 @@ func TestCrossClientScopes(t *testing.T) {
if !reflect.DeepEqual(idToken.Audience, expAudience) {
t.Errorf("expected audience %q, got %q", expAudience, idToken.Audience)
}
}
if gotState := q.Get("state"); gotState != state {
t.Errorf("state did not match, want=%q got=%q", state, gotState)
@ -848,6 +851,8 @@ func TestCrossClientScopes(t *testing.T) {
if err != nil {
t.Fatalf("get failed: %v", err)
}
defer resp.Body.Close()
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
t.Fatal(err)
}
@ -921,7 +926,6 @@ func TestCrossClientScopesWithAzpInAudienceByDefault(t *testing.T) {
if !reflect.DeepEqual(idToken.Audience, expAudience) {
t.Errorf("expected audience %q, got %q", expAudience, idToken.Audience)
}
}
if gotState := q.Get("state"); gotState != state {
t.Errorf("state did not match, want=%q got=%q", state, gotState)
@ -969,6 +973,8 @@ func TestCrossClientScopesWithAzpInAudienceByDefault(t *testing.T) {
if err != nil {
t.Fatalf("get failed: %v", err)
}
defer resp.Body.Close()
if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil {
t.Fatal(err)
}
@ -1058,7 +1064,6 @@ func TestPasswordDB(t *testing.T) {
t.Errorf("%s: %s", tc.name, diff)
}
}
}
func TestPasswordDBUsernamePrompt(t *testing.T) {
@ -1225,9 +1230,11 @@ func TestRefreshTokenFlow(t *testing.T) {
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)
}
defer resp.Body.Close()
tok := &oauth2.Token{
RefreshToken: oauth2Client.token.RefreshToken,
@ -1235,9 +1242,11 @@ func TestRefreshTokenFlow(t *testing.T) {
}
// 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)
}
defer resp.Body.Close()
// try to refresh expired token with old refresh token.
if _, err := oauth2Client.config.TokenSource(ctx, tok).Token(); err == nil {

View file

@ -47,19 +47,6 @@ type webConfig struct {
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 {
stat, err := os.Stat(dir)
if err != nil {
@ -189,7 +176,6 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
//assetPath is static/main.css
//relativeURL("/dex", "/dex/auth", "static/main.css") = "../static/main.css"
func relativeURL(serverPath, reqPath, assetPath string) string {
splitPath := func(p string) []string {
res := []string{}
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)
// Remove common prefix of request path with server path
// nolint: ineffassign
server, req = stripCommonParts(server, req)
// Remove common prefix of request path with asset path
@ -246,6 +233,7 @@ type connectorInfo struct {
ID string
Name string
URL string
Type string
}
type byName []connectorInfo

View file

@ -161,6 +161,8 @@ func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
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) {
@ -214,7 +216,7 @@ func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
got, err := s.GetAuthCode(a1.ID)
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() {
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)
mustBeErrNotFound(t, "password", err)
}
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 {
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 {
t.Errorf("expected auth code to be GC'd")
t.Errorf("expected auth request to be GC'd")
} else if err != storage.ErrNotFound {
t.Errorf("expected storage.ErrNotFound, got %v", err)
}

View file

@ -135,8 +135,8 @@ func testKeysConcurrentUpdate(t *testing.T, s storage.Storage) {
for i := 0; i < 2; i++ {
n := time.Now().UTC().Round(time.Second)
keys1 := storage.Keys{
SigningKey: jsonWebKeys[0].Private,
SigningKeyPub: jsonWebKeys[0].Public,
SigningKey: jsonWebKeys[i].Private,
SigningKeyPub: jsonWebKeys[i].Public,
NextRotation: n,
}

View file

@ -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) {
var current RefreshToken
if len(currentValue) > 0 {
if err := json.Unmarshal([]byte(currentValue), &current); err != nil {
if err := json.Unmarshal(currentValue, &current); err != nil {
return nil, err
}
}

View file

@ -55,14 +55,14 @@ type client struct {
}
// idToName maps an arbitrary ID, such as an email or client ID to a Kubernetes object name.
func (c *client) idToName(s string) string {
return idToName(s, c.hash)
func (cli *client) idToName(s string) string {
return idToName(s, cli.hash)
}
// 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.
func (c *client) offlineTokenName(userID string, connID string) string {
return offlineTokenName(userID, connID, c.hash)
func (cli *client) offlineTokenName(userID string, connID string) string {
return offlineTokenName(userID, connID, cli.hash)
}
// 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)), "=")
}
func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
func (cli *client) urlFor(apiVersion, namespace, resource, name string) string {
basePath := "apis/"
if apiVersion == "v1" {
basePath = "api/"
@ -91,10 +91,10 @@ func (c *client) urlFor(apiVersion, namespace, resource, name string) string {
} else {
p = path.Join(basePath, apiVersion, resource, name)
}
if strings.HasSuffix(c.baseURL, "/") {
return c.baseURL + p
if strings.HasSuffix(cli.baseURL, "/") {
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
@ -156,13 +156,13 @@ func closeResp(r *http.Response) {
r.Body.Close()
}
func (c *client) get(resource, name string, v interface{}) error {
return c.getResource(c.apiVersion, c.namespace, resource, name, v)
func (cli *client) get(resource, name string, v interface{}) error {
return cli.getResource(cli.apiVersion, cli.namespace, resource, name, v)
}
func (c *client) getResource(apiVersion, namespace, resource, name string, v interface{}) error {
url := c.urlFor(apiVersion, namespace, resource, name)
resp, err := c.client.Get(url)
func (cli *client) getResource(apiVersion, namespace, resource, name string, v interface{}) error {
url := cli.urlFor(apiVersion, namespace, resource, name)
resp, err := cli.client.Get(url)
if err != nil {
return err
}
@ -173,22 +173,22 @@ func (c *client) getResource(apiVersion, namespace, resource, name string, v int
return json.NewDecoder(resp.Body).Decode(v)
}
func (c *client) list(resource string, v interface{}) error {
return c.get(resource, "", v)
func (cli *client) list(resource string, v interface{}) error {
return cli.get(resource, "", v)
}
func (c *client) post(resource string, v interface{}) error {
return c.postResource(c.apiVersion, c.namespace, resource, v)
func (cli *client) post(resource string, v interface{}) error {
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)
if err != nil {
return fmt.Errorf("marshal object: %v", err)
}
url := c.urlFor(apiVersion, namespace, resource, "")
resp, err := c.client.Post(url, "application/json", bytes.NewReader(body))
url := cli.urlFor(apiVersion, namespace, resource, "")
resp, err := cli.client.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
@ -196,13 +196,13 @@ func (c *client) postResource(apiVersion, namespace, resource string, v interfac
return checkHTTPErr(resp, http.StatusCreated)
}
func (c *client) delete(resource, name string) error {
url := c.urlFor(c.apiVersion, c.namespace, resource, name)
func (cli *client) delete(resource, name string) error {
url := cli.urlFor(cli.apiVersion, cli.namespace, resource, name)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return fmt.Errorf("create delete request: %v", err)
}
resp, err := c.client.Do(req)
resp, err := cli.client.Do(req)
if err != nil {
return fmt.Errorf("delete request: %v", err)
}
@ -210,7 +210,7 @@ func (c *client) delete(resource, name string) error {
return checkHTTPErr(resp, http.StatusOK)
}
func (c *client) deleteAll(resource string) error {
func (cli *client) deleteAll(resource string) error {
var list struct {
k8sapi.TypeMeta `json:",inline"`
k8sapi.ListMeta `json:"metadata,omitempty"`
@ -219,24 +219,24 @@ func (c *client) deleteAll(resource string) error {
k8sapi.ObjectMeta `json:"metadata,omitempty"`
} `json:"items"`
}
if err := c.list(resource, &list); err != nil {
if err := cli.list(resource, &list); err != nil {
return err
}
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 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)
if err != nil {
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))
if err != nil {
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)))
resp, err := c.client.Do(req)
resp, err := cli.client.Do(req)
if err != nil {
return fmt.Errorf("patch request: %v", err)
}

View file

@ -43,7 +43,7 @@ type CustomResourceDefinitionNames struct {
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
const (

View file

@ -1,39 +1,104 @@
package kubernetes
import (
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"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/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) {
loadClient(t)
func TestStorage(t *testing.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{
KubeConfigFile: os.Getenv(testKubeConfigEnv),
}
if config.KubeConfigFile == "" {
t.Skipf("test environment variable %q not set, skipping", testKubeConfigEnv)
KubeConfigFile: f.Name(),
}
logger := &logrus.Logger{
Out: os.Stderr,
Formatter: &logrus.TextFormatter{DisableColors: true},
Level: logrus.DebugLevel,
}
s, err := config.open(logger, true)
if err != nil {
t.Fatal(err)
client, err := config.open(logger, true)
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) {
@ -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)
}

View file

@ -308,10 +308,17 @@ func (s *MySQL) open(logger log.Logger) (*conn, error) {
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()
if err != nil {
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 8.0 doesn't have tx_isolation at all.

View file

@ -169,7 +169,6 @@ func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest)
}
return nil
})
}
func (c *conn) GetAuthRequest(id string) (storage.AuthRequest, error) {

View file

@ -68,12 +68,12 @@ body {
background-size: contain;
}
.dex-btn-icon--bitbucket {
.dex-btn-icon--bitbucket-cloud {
background-color: #205081;
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-image: url(../static/img/ldap-icon.svg);
}

View file

@ -7,7 +7,7 @@
<div class="theme-form-row">
<a href="{{ $c.URL }}" target="_self">
<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>
</button>
</a>