Merge branch 'main' into fix-getting-started4
This commit is contained in:
commit
c83cec341b
@ -60,7 +60,7 @@ issues:
|
|||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
funlen:
|
funlen:
|
||||||
lines: 125
|
lines: 150
|
||||||
statements: 50
|
statements: 50
|
||||||
goheader:
|
goheader:
|
||||||
template: |-
|
template: |-
|
||||||
|
@ -13,6 +13,7 @@ RUN go mod download
|
|||||||
# Copy only the production source code to avoid cache misses when editing other files
|
# Copy only the production source code to avoid cache misses when editing other files
|
||||||
COPY generated ./generated
|
COPY generated ./generated
|
||||||
COPY cmd ./cmd
|
COPY cmd ./cmd
|
||||||
|
COPY pkg ./pkg
|
||||||
COPY internal ./internal
|
COPY internal ./internal
|
||||||
COPY tools ./tools
|
COPY tools ./tools
|
||||||
COPY hack ./hack
|
COPY hack ./hack
|
||||||
|
11
apis/supervisor/idp/v1alpha1/types_tls.go.tmpl
Normal file
11
apis/supervisor/idp/v1alpha1/types_tls.go.tmpl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
// Configuration for TLS parameters related to identity provider integration.
|
||||||
|
type TLSSpec struct {
|
||||||
|
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||||
|
// +optional
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||||
|
}
|
@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://`
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
|
|
||||||
|
// TLS configuration for discovery/JWKS requests to the issuer.
|
||||||
|
// +optional
|
||||||
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
|
|
||||||
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
||||||
// parameters to be used with this OIDC identity provider.
|
// parameters to be used with this OIDC identity provider.
|
||||||
// +optional
|
// +optional
|
||||||
|
@ -4,7 +4,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@ -13,8 +18,8 @@ import (
|
|||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
"k8s.io/klog/v2/klogr"
|
"k8s.io/klog/v2/klogr"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
"go.pinniped.dev/internal/oidcclient/filesession"
|
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint: gochecknoinits
|
//nolint: gochecknoinits
|
||||||
@ -36,6 +41,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid
|
|||||||
scopes []string
|
scopes []string
|
||||||
skipBrowser bool
|
skipBrowser bool
|
||||||
sessionCachePath string
|
sessionCachePath string
|
||||||
|
caBundlePaths []string
|
||||||
debugSessionCache bool
|
debugSessionCache bool
|
||||||
)
|
)
|
||||||
cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.")
|
cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.")
|
||||||
@ -44,6 +50,7 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid
|
|||||||
cmd.Flags().StringSliceVar(&scopes, "scopes", []string{"offline_access", "openid", "email", "profile"}, "OIDC scopes to request during login.")
|
cmd.Flags().StringSliceVar(&scopes, "scopes", []string{"offline_access", "openid", "email", "profile"}, "OIDC scopes to request during login.")
|
||||||
cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).")
|
cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).")
|
||||||
cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.")
|
cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.")
|
||||||
|
cmd.Flags().StringSliceVar(&caBundlePaths, "ca-bundle", nil, "Path to TLS certificate authority bundle (PEM format, optional, can be repeated).")
|
||||||
cmd.Flags().BoolVar(&debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache.")
|
cmd.Flags().BoolVar(&debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache.")
|
||||||
mustMarkHidden(&cmd, "debug-session-cache")
|
mustMarkHidden(&cmd, "debug-session-cache")
|
||||||
mustMarkRequired(&cmd, "issuer", "client-id")
|
mustMarkRequired(&cmd, "issuer", "client-id")
|
||||||
@ -80,6 +87,27 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oid
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(caBundlePaths) > 0 {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
for _, p := range caBundlePaths {
|
||||||
|
pem, err := ioutil.ReadFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not read --ca-bundle: %w", err)
|
||||||
|
}
|
||||||
|
pool.AppendCertsFromPEM(pem)
|
||||||
|
}
|
||||||
|
tlsConfig := tls.Config{
|
||||||
|
RootCAs: pool,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
opts = append(opts, oidcclient.WithClient(&http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSClientConfig: &tlsConfig,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
tok, err := loginFunc(issuer, clientID, opts...)
|
tok, err := loginFunc(issuer, clientID, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/here"
|
"go.pinniped.dev/internal/here"
|
||||||
"go.pinniped.dev/internal/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoginOIDCCommand(t *testing.T) {
|
func TestLoginOIDCCommand(t *testing.T) {
|
||||||
@ -40,6 +40,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
|||||||
oidc --issuer ISSUER --client-id CLIENT_ID [flags]
|
oidc --issuer ISSUER --client-id CLIENT_ID [flags]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
|
--ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated).
|
||||||
--client-id string OpenID Connect client ID.
|
--client-id string OpenID Connect client ID.
|
||||||
-h, --help help for oidc
|
-h, --help help for oidc
|
||||||
--issuer string OpenID Connect issuer URL.
|
--issuer string OpenID Connect issuer URL.
|
||||||
|
@ -98,6 +98,15 @@ spec:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
pattern: ^https://
|
pattern: ^https://
|
||||||
type: string
|
type: string
|
||||||
|
tls:
|
||||||
|
description: TLS configuration for discovery/JWKS requests to the
|
||||||
|
issuer.
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: X.509 Certificate Authority (base64-encoded PEM bundle).
|
||||||
|
If omitted, a default set of system roots will be trusted.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- client
|
- client
|
||||||
- issuer
|
- issuer
|
||||||
|
18
generated/1.17/README.adoc
generated
18
generated/1.17/README.adoc
generated
@ -373,6 +373,23 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec"]
|
||||||
|
==== TLSSpec
|
||||||
|
|
||||||
|
Configuration for TLS parameters related to identity provider integration.
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-upstreamoidcproviderspec[$$UpstreamOIDCProviderSpec$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
|
||||||
==== UpstreamOIDCProvider
|
==== UpstreamOIDCProvider
|
||||||
|
|
||||||
@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider.
|
|||||||
|===
|
|===
|
||||||
| Field | Description
|
| Field | Description
|
||||||
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration.
|
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration.
|
||||||
|
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer.
|
||||||
| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider.
|
| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider.
|
||||||
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider.
|
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider.
|
||||||
| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider.
|
| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider.
|
||||||
|
11
generated/1.17/apis/supervisor/idp/v1alpha1/types_tls.go
generated
Normal file
11
generated/1.17/apis/supervisor/idp/v1alpha1/types_tls.go
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
// Configuration for TLS parameters related to identity provider integration.
|
||||||
|
type TLSSpec struct {
|
||||||
|
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||||
|
// +optional
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||||
|
}
|
@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://`
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
|
|
||||||
|
// TLS configuration for discovery/JWKS requests to the issuer.
|
||||||
|
// +optional
|
||||||
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
|
|
||||||
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
||||||
// parameters to be used with this OIDC identity provider.
|
// parameters to be used with this OIDC identity provider.
|
||||||
// +optional
|
// +optional
|
||||||
|
@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec.
|
||||||
|
func (in *TLSSpec) DeepCopy() *TLSSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(TLSSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
|
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@ -145,6 +161,11 @@ func (in *UpstreamOIDCProviderList) DeepCopyObject() runtime.Object {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
|
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.TLS != nil {
|
||||||
|
in, out := &in.TLS, &out.TLS
|
||||||
|
*out = new(TLSSpec)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
|
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
|
||||||
out.Claims = in.Claims
|
out.Claims = in.Claims
|
||||||
out.Client = in.Client
|
out.Client = in.Client
|
||||||
|
@ -98,6 +98,15 @@ spec:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
pattern: ^https://
|
pattern: ^https://
|
||||||
type: string
|
type: string
|
||||||
|
tls:
|
||||||
|
description: TLS configuration for discovery/JWKS requests to the
|
||||||
|
issuer.
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: X.509 Certificate Authority (base64-encoded PEM bundle).
|
||||||
|
If omitted, a default set of system roots will be trusted.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- client
|
- client
|
||||||
- issuer
|
- issuer
|
||||||
|
18
generated/1.18/README.adoc
generated
18
generated/1.18/README.adoc
generated
@ -373,6 +373,23 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec"]
|
||||||
|
==== TLSSpec
|
||||||
|
|
||||||
|
Configuration for TLS parameters related to identity provider integration.
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-upstreamoidcproviderspec[$$UpstreamOIDCProviderSpec$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
|
||||||
==== UpstreamOIDCProvider
|
==== UpstreamOIDCProvider
|
||||||
|
|
||||||
@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider.
|
|||||||
|===
|
|===
|
||||||
| Field | Description
|
| Field | Description
|
||||||
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration.
|
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration.
|
||||||
|
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer.
|
||||||
| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider.
|
| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider.
|
||||||
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider.
|
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider.
|
||||||
| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider.
|
| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider.
|
||||||
|
11
generated/1.18/apis/supervisor/idp/v1alpha1/types_tls.go
generated
Normal file
11
generated/1.18/apis/supervisor/idp/v1alpha1/types_tls.go
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
// Configuration for TLS parameters related to identity provider integration.
|
||||||
|
type TLSSpec struct {
|
||||||
|
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||||
|
// +optional
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||||
|
}
|
@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://`
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
|
|
||||||
|
// TLS configuration for discovery/JWKS requests to the issuer.
|
||||||
|
// +optional
|
||||||
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
|
|
||||||
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
||||||
// parameters to be used with this OIDC identity provider.
|
// parameters to be used with this OIDC identity provider.
|
||||||
// +optional
|
// +optional
|
||||||
|
@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec.
|
||||||
|
func (in *TLSSpec) DeepCopy() *TLSSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(TLSSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
|
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@ -145,6 +161,11 @@ func (in *UpstreamOIDCProviderList) DeepCopyObject() runtime.Object {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
|
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.TLS != nil {
|
||||||
|
in, out := &in.TLS, &out.TLS
|
||||||
|
*out = new(TLSSpec)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
|
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
|
||||||
out.Claims = in.Claims
|
out.Claims = in.Claims
|
||||||
out.Client = in.Client
|
out.Client = in.Client
|
||||||
|
@ -98,6 +98,15 @@ spec:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
pattern: ^https://
|
pattern: ^https://
|
||||||
type: string
|
type: string
|
||||||
|
tls:
|
||||||
|
description: TLS configuration for discovery/JWKS requests to the
|
||||||
|
issuer.
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: X.509 Certificate Authority (base64-encoded PEM bundle).
|
||||||
|
If omitted, a default set of system roots will be trusted.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- client
|
- client
|
||||||
- issuer
|
- issuer
|
||||||
|
18
generated/1.19/README.adoc
generated
18
generated/1.19/README.adoc
generated
@ -373,6 +373,23 @@ OIDCClient contains information about an OIDC client (e.g., client ID and client
|
|||||||
|===
|
|===
|
||||||
|
|
||||||
|
|
||||||
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec"]
|
||||||
|
==== TLSSpec
|
||||||
|
|
||||||
|
Configuration for TLS parameters related to identity provider integration.
|
||||||
|
|
||||||
|
.Appears In:
|
||||||
|
****
|
||||||
|
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-upstreamoidcproviderspec[$$UpstreamOIDCProviderSpec$$]
|
||||||
|
****
|
||||||
|
|
||||||
|
[cols="25a,75a", options="header"]
|
||||||
|
|===
|
||||||
|
| Field | Description
|
||||||
|
| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||||
|
|===
|
||||||
|
|
||||||
|
|
||||||
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
|
[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-upstreamoidcprovider"]
|
||||||
==== UpstreamOIDCProvider
|
==== UpstreamOIDCProvider
|
||||||
|
|
||||||
@ -409,6 +426,7 @@ Spec for configuring an OIDC identity provider.
|
|||||||
|===
|
|===
|
||||||
| Field | Description
|
| Field | Description
|
||||||
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration.
|
| *`issuer`* __string__ | Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch /.well-known/openid-configuration.
|
||||||
|
| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for discovery/JWKS requests to the issuer.
|
||||||
| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider.
|
| *`authorizationConfig`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig[$$OIDCAuthorizationConfig$$]__ | AuthorizationConfig holds information about how to form the OAuth2 authorization request parameters to be used with this OIDC identity provider.
|
||||||
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider.
|
| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcclaims[$$OIDCClaims$$]__ | Claims provides the names of token claims that will be used when inspecting an identity from this OIDC identity provider.
|
||||||
| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider.
|
| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcclient[$$OIDCClient$$]__ | OIDCClient contains OIDC client information to be used used with this OIDC identity provider.
|
||||||
|
11
generated/1.19/apis/supervisor/idp/v1alpha1/types_tls.go
generated
Normal file
11
generated/1.19/apis/supervisor/idp/v1alpha1/types_tls.go
generated
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
// Configuration for TLS parameters related to identity provider integration.
|
||||||
|
type TLSSpec struct {
|
||||||
|
// X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted.
|
||||||
|
// +optional
|
||||||
|
CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"`
|
||||||
|
}
|
@ -75,6 +75,10 @@ type UpstreamOIDCProviderSpec struct {
|
|||||||
// +kubebuilder:validation:Pattern=`^https://`
|
// +kubebuilder:validation:Pattern=`^https://`
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
|
|
||||||
|
// TLS configuration for discovery/JWKS requests to the issuer.
|
||||||
|
// +optional
|
||||||
|
TLS *TLSSpec `json:"tls,omitempty"`
|
||||||
|
|
||||||
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
// AuthorizationConfig holds information about how to form the OAuth2 authorization request
|
||||||
// parameters to be used with this OIDC identity provider.
|
// parameters to be used with this OIDC identity provider.
|
||||||
// +optional
|
// +optional
|
||||||
|
@ -81,6 +81,22 @@ func (in *OIDCClient) DeepCopy() *OIDCClient {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *TLSSpec) DeepCopyInto(out *TLSSpec) {
|
||||||
|
*out = *in
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec.
|
||||||
|
func (in *TLSSpec) DeepCopy() *TLSSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(TLSSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
|
func (in *UpstreamOIDCProvider) DeepCopyInto(out *UpstreamOIDCProvider) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@ -145,6 +161,11 @@ func (in *UpstreamOIDCProviderList) DeepCopyObject() runtime.Object {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
|
func (in *UpstreamOIDCProviderSpec) DeepCopyInto(out *UpstreamOIDCProviderSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.TLS != nil {
|
||||||
|
in, out := &in.TLS, &out.TLS
|
||||||
|
*out = new(TLSSpec)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
|
in.AuthorizationConfig.DeepCopyInto(&out.AuthorizationConfig)
|
||||||
out.Claims = in.Claims
|
out.Claims = in.Claims
|
||||||
out.Client = in.Client
|
out.Client = in.Client
|
||||||
|
@ -98,6 +98,15 @@ spec:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
pattern: ^https://
|
pattern: ^https://
|
||||||
type: string
|
type: string
|
||||||
|
tls:
|
||||||
|
description: TLS configuration for discovery/JWKS requests to the
|
||||||
|
issuer.
|
||||||
|
properties:
|
||||||
|
certificateAuthorityData:
|
||||||
|
description: X.509 Certificate Authority (base64-encoded PEM bundle).
|
||||||
|
If omitted, a default set of system roots will be trusted.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- client
|
- client
|
||||||
- issuer
|
- issuer
|
||||||
|
@ -19,23 +19,25 @@ local_resource(
|
|||||||
)
|
)
|
||||||
|
|
||||||
#####################################################################################################
|
#####################################################################################################
|
||||||
# Dex
|
# Test IDP (Dex + cert generation + squid proxy)
|
||||||
#
|
#
|
||||||
|
|
||||||
# Render the Dex installation manifest using ytt.
|
# Render the IDP installation manifest using ytt.
|
||||||
k8s_yaml(local(['ytt','--file', '../../../test/deploy/dex']))
|
k8s_yaml(local(['ytt','--file', '../../../test/deploy/dex']))
|
||||||
# Tell tilt to watch all of those files for changes.
|
# Tell tilt to watch all of those files for changes.
|
||||||
watch_file('../../../test/deploy/dex')
|
watch_file('../../../test/deploy/dex')
|
||||||
|
|
||||||
# Collect all the deployed Dex resources under a "dex" resource tab.
|
k8s_resource(objects=['dex:namespace'], new_name='dex-ns')
|
||||||
k8s_resource(
|
k8s_resource(workload='cert-issuer', resource_deps=['dex-ns'], objects=[
|
||||||
workload='dex', # this is the deployment name
|
'cert-issuer:serviceaccount',
|
||||||
objects=[
|
'cert-issuer:role',
|
||||||
# these are the objects that would otherwise appear in the "uncategorized" tab in the tilt UI
|
'cert-issuer:rolebinding',
|
||||||
'dex:namespace',
|
])
|
||||||
'dex-config:configmap',
|
k8s_resource(workload='proxy', resource_deps=['dex-ns'])
|
||||||
],
|
k8s_resource(workload='dex', resource_deps=['dex-ns', 'cert-issuer'], objects=[
|
||||||
)
|
'dex-config:configmap',
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
#####################################################################################################
|
#####################################################################################################
|
||||||
# Local-user-authenticator app
|
# Local-user-authenticator app
|
||||||
@ -186,6 +188,6 @@ k8s_resource(
|
|||||||
local_resource(
|
local_resource(
|
||||||
'test-env',
|
'test-env',
|
||||||
'TILT_MODE=yes ../../prepare-for-integration-tests.sh',
|
'TILT_MODE=yes ../../prepare-for-integration-tests.sh',
|
||||||
resource_deps=['local-user-auth', 'concierge', 'supervisor'],
|
resource_deps=['local-user-auth', 'concierge', 'supervisor', 'dex', 'proxy'],
|
||||||
deps=['../../prepare-for-integration-tests.sh'],
|
deps=['../../prepare-for-integration-tests.sh'],
|
||||||
)
|
)
|
||||||
|
@ -265,6 +265,11 @@ if ! tilt_mode; then
|
|||||||
popd >/dev/null
|
popd >/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#
|
||||||
|
# Download the test CA bundle that was generated in the Dex pod.
|
||||||
|
#
|
||||||
|
test_ca_bundle_pem="$(kubectl get secrets -n dex certs -o go-template='{{index .data "ca.pem" | base64decode}}')"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Create the environment file
|
# Create the environment file
|
||||||
#
|
#
|
||||||
@ -286,7 +291,9 @@ export PINNIPED_TEST_SUPERVISOR_APP_NAME=${supervisor_app_name}
|
|||||||
export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}'
|
export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}'
|
||||||
export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345"
|
export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345"
|
||||||
export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344"
|
export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344"
|
||||||
export PINNIPED_TEST_CLI_OIDC_ISSUER=http://127.0.0.1:12346/dex
|
export PINNIPED_TEST_PROXY=http://127.0.0.1:12346
|
||||||
|
export PINNIPED_TEST_CLI_OIDC_ISSUER=https://dex.dex.svc.cluster.local/dex
|
||||||
|
export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}"
|
||||||
export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli
|
export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli
|
||||||
export PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT=48095
|
export PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT=48095
|
||||||
export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com
|
export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com
|
||||||
|
@ -6,7 +6,11 @@ package upstreamwatcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
@ -48,10 +52,12 @@ const (
|
|||||||
reasonMissingKeys = "SecretMissingKeys"
|
reasonMissingKeys = "SecretMissingKeys"
|
||||||
reasonSuccess = "Success"
|
reasonSuccess = "Success"
|
||||||
reasonUnreachable = "Unreachable"
|
reasonUnreachable = "Unreachable"
|
||||||
|
reasonInvalidTLSConfig = "InvalidTLSConfig"
|
||||||
reasonInvalidResponse = "InvalidResponse"
|
reasonInvalidResponse = "InvalidResponse"
|
||||||
|
|
||||||
// Errors that are generated by our reconcile process.
|
// Errors that are generated by our reconcile process.
|
||||||
errFailureStatus = constable.Error("UpstreamOIDCProvider has a failing condition")
|
errFailureStatus = constable.Error("UpstreamOIDCProvider has a failing condition")
|
||||||
|
errNoCertificates = constable.Error("no certificates found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// IDPCache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations.
|
// IDPCache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations.
|
||||||
@ -59,13 +65,39 @@ type IDPCache interface {
|
|||||||
SetIDPList([]provider.UpstreamOIDCIdentityProvider)
|
SetIDPList([]provider.UpstreamOIDCIdentityProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lruValidatorCache caches the *oidc.Provider associated with a particular issuer/TLS configuration.
|
||||||
|
type lruValidatorCache struct{ cache *cache.Expiring }
|
||||||
|
|
||||||
|
func (c *lruValidatorCache) getProvider(spec *v1alpha1.UpstreamOIDCProviderSpec) *oidc.Provider {
|
||||||
|
if result, ok := c.cache.Get(c.cacheKey(spec)); ok {
|
||||||
|
return result.(*oidc.Provider)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lruValidatorCache) putProvider(spec *v1alpha1.UpstreamOIDCProviderSpec, provider *oidc.Provider) {
|
||||||
|
c.cache.Set(c.cacheKey(spec), provider, validatorCacheTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lruValidatorCache) cacheKey(spec *v1alpha1.UpstreamOIDCProviderSpec) interface{} {
|
||||||
|
var key struct{ issuer, caBundle string }
|
||||||
|
key.issuer = spec.Issuer
|
||||||
|
if spec.TLS != nil {
|
||||||
|
key.caBundle = spec.TLS.CertificateAuthorityData
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
type controller struct {
|
type controller struct {
|
||||||
cache IDPCache
|
cache IDPCache
|
||||||
log logr.Logger
|
log logr.Logger
|
||||||
client pinnipedclientset.Interface
|
client pinnipedclientset.Interface
|
||||||
providers idpinformers.UpstreamOIDCProviderInformer
|
providers idpinformers.UpstreamOIDCProviderInformer
|
||||||
secrets corev1informers.SecretInformer
|
secrets corev1informers.SecretInformer
|
||||||
validatorCache *cache.Expiring
|
validatorCache interface {
|
||||||
|
getProvider(spec *v1alpha1.UpstreamOIDCProviderSpec) *oidc.Provider
|
||||||
|
putProvider(spec *v1alpha1.UpstreamOIDCProviderSpec, provider *oidc.Provider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New instantiates a new controllerlib.Controller which will populate the provided IDPCache.
|
// New instantiates a new controllerlib.Controller which will populate the provided IDPCache.
|
||||||
@ -82,7 +114,7 @@ func New(
|
|||||||
client: client,
|
client: client,
|
||||||
providers: providers,
|
providers: providers,
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
validatorCache: cache.NewExpiring(),
|
validatorCache: &lruValidatorCache{cache: cache.NewExpiring()},
|
||||||
}
|
}
|
||||||
filter := pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue())
|
filter := pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue())
|
||||||
return controllerlib.New(
|
return controllerlib.New(
|
||||||
@ -197,15 +229,22 @@ func (c *controller) validateSecret(upstream *v1alpha1.UpstreamOIDCProvider, res
|
|||||||
// validateIssuer validates the .spec.issuer field, performs OIDC discovery, and returns the appropriate OIDCDiscoverySucceeded condition.
|
// validateIssuer validates the .spec.issuer field, performs OIDC discovery, and returns the appropriate OIDCDiscoverySucceeded condition.
|
||||||
func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, result *provider.UpstreamOIDCIdentityProvider) *v1alpha1.Condition {
|
func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, result *provider.UpstreamOIDCIdentityProvider) *v1alpha1.Condition {
|
||||||
// Get the provider (from cache if possible).
|
// Get the provider (from cache if possible).
|
||||||
var discoveredProvider *oidc.Provider
|
discoveredProvider := c.validatorCache.getProvider(&upstream.Spec)
|
||||||
if cached, ok := c.validatorCache.Get(upstream.Spec.Issuer); ok {
|
|
||||||
discoveredProvider = cached.(*oidc.Provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache.
|
// If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache.
|
||||||
if discoveredProvider == nil {
|
if discoveredProvider == nil {
|
||||||
var err error
|
tlsConfig, err := getTLSConfig(upstream)
|
||||||
discoveredProvider, err = oidc.NewProvider(ctx, upstream.Spec.Issuer)
|
if err != nil {
|
||||||
|
return &v1alpha1.Condition{
|
||||||
|
Type: typeOIDCDiscoverySucceeded,
|
||||||
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
Reason: reasonInvalidTLSConfig,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||||
|
|
||||||
|
discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &v1alpha1.Condition{
|
return &v1alpha1.Condition{
|
||||||
Type: typeOIDCDiscoverySucceeded,
|
Type: typeOIDCDiscoverySucceeded,
|
||||||
@ -216,7 +255,7 @@ func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.Upst
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the cache with the newly discovered value.
|
// Update the cache with the newly discovered value.
|
||||||
c.validatorCache.Set(upstream.Spec.Issuer, discoveredProvider, validatorCacheTTL)
|
c.validatorCache.putProvider(&upstream.Spec, discoveredProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse out and validate the discovered authorize endpoint.
|
// Parse out and validate the discovered authorize endpoint.
|
||||||
@ -248,6 +287,28 @@ func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.Upst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTLSConfig(upstream *v1alpha1.UpstreamOIDCProvider) (*tls.Config, error) {
|
||||||
|
result := tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
|
||||||
|
if upstream.Spec.TLS == nil || upstream.Spec.TLS.CertificateAuthorityData == "" {
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle, err := base64.StdEncoding.DecodeString(upstream.Spec.TLS.CertificateAuthorityData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.RootCAs = x509.NewCertPool()
|
||||||
|
if !result.RootCAs.AppendCertsFromPEM(bundle) {
|
||||||
|
return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", errNoCertificates)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *controller) updateStatus(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, conditions []*v1alpha1.Condition) {
|
func (c *controller) updateStatus(ctx context.Context, upstream *v1alpha1.UpstreamOIDCProvider, conditions []*v1alpha1.Condition) {
|
||||||
log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name)
|
log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name)
|
||||||
updated := upstream.DeepCopy()
|
updated := upstream.DeepCopy()
|
||||||
|
@ -5,9 +5,9 @@ package upstreamwatcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -25,6 +25,7 @@ import (
|
|||||||
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
|
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
|
||||||
"go.pinniped.dev/internal/controllerlib"
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
|
"go.pinniped.dev/internal/testutil"
|
||||||
"go.pinniped.dev/internal/testutil/testlogger"
|
"go.pinniped.dev/internal/testutil/testlogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,7 +35,8 @@ func TestController(t *testing.T) {
|
|||||||
earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC())
|
earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC())
|
||||||
|
|
||||||
// Start another test server that answers discovery successfully.
|
// Start another test server that answers discovery successfully.
|
||||||
testIssuer := newTestIssuer(t)
|
testIssuerCA, testIssuerURL := newTestIssuer(t)
|
||||||
|
testIssuerCABase64 := base64.StdEncoding.EncodeToString([]byte(testIssuerCA))
|
||||||
testIssuerAuthorizeURL, err := url.Parse("https://example.com/authorize")
|
testIssuerAuthorizeURL, err := url.Parse("https://example.com/authorize")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -65,7 +67,8 @@ func TestController(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: testIssuer.URL,
|
Issuer: testIssuerURL,
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||||
},
|
},
|
||||||
@ -106,7 +109,8 @@ func TestController(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: testIssuer.URL,
|
Issuer: testIssuerURL,
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||||
},
|
},
|
||||||
@ -151,7 +155,8 @@ func TestController(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: testIssuer.URL,
|
Issuer: testIssuerURL,
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||||
},
|
},
|
||||||
@ -190,6 +195,102 @@ func TestController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "TLS CA bundle is invalid base64",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
|
||||||
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
|
Issuer: testIssuerURL,
|
||||||
|
TLS: &v1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: "invalid-base64",
|
||||||
|
},
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`upstream-observer "error"="UpstreamOIDCProvider has a failing condition" "msg"="found failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []provider.UpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.UpstreamOIDCProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.UpstreamOIDCProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "InvalidTLSConfig",
|
||||||
|
Message: `spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TLS CA bundle does not have any certificates",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
|
||||||
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
|
Issuer: testIssuerURL,
|
||||||
|
TLS: &v1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("not-a-pem-ca-bundle")),
|
||||||
|
},
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`upstream-observer "error"="UpstreamOIDCProvider has a failing condition" "msg"="found failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []provider.UpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.UpstreamOIDCProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.UpstreamOIDCProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "InvalidTLSConfig",
|
||||||
|
Message: `spec.certificateAuthorityData is invalid: no certificates found`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "issuer is invalid URL",
|
name: "issuer is invalid URL",
|
||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
@ -240,7 +341,8 @@ func TestController(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: testIssuer.URL + "/invalid",
|
Issuer: testIssuerURL + "/invalid",
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||||
},
|
},
|
||||||
@ -285,7 +387,8 @@ func TestController(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: testIssuer.URL + "/insecure",
|
Issuer: testIssuerURL + "/insecure",
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||||
},
|
},
|
||||||
@ -330,7 +433,8 @@ func TestController(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test-name"},
|
||||||
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: testIssuer.URL,
|
Issuer: testIssuerURL,
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: append(testAdditionalScopes, "xyz", "openid")},
|
||||||
},
|
},
|
||||||
@ -373,7 +477,8 @@ func TestController(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.UpstreamOIDCProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
|
||||||
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
Spec: v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: testIssuer.URL,
|
Issuer: testIssuerURL,
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{AdditionalScopes: testAdditionalScopes},
|
||||||
},
|
},
|
||||||
@ -486,10 +591,9 @@ func normalizeUpstreams(upstreams []v1alpha1.UpstreamOIDCProvider, now metav1.Ti
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestIssuer(t *testing.T) *httptest.Server {
|
func newTestIssuer(t *testing.T) (string, string) {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
testServer := httptest.NewServer(mux)
|
caBundlePEM, testURL := testutil.TLSTestServer(t, mux.ServeHTTP)
|
||||||
t.Cleanup(testServer.Close)
|
|
||||||
|
|
||||||
type providerJSON struct {
|
type providerJSON struct {
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
@ -502,7 +606,7 @@ func newTestIssuer(t *testing.T) *httptest.Server {
|
|||||||
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
Issuer: testServer.URL,
|
Issuer: testURL,
|
||||||
AuthURL: "https://example.com/authorize",
|
AuthURL: "https://example.com/authorize",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -511,7 +615,7 @@ func newTestIssuer(t *testing.T) *httptest.Server {
|
|||||||
mux.HandleFunc("/invalid/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/invalid/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
Issuer: testServer.URL + "/invalid",
|
Issuer: testURL + "/invalid",
|
||||||
AuthURL: "%",
|
AuthURL: "%",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -520,10 +624,10 @@ func newTestIssuer(t *testing.T) *httptest.Server {
|
|||||||
mux.HandleFunc("/insecure/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/insecure/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
Issuer: testServer.URL + "/insecure",
|
Issuer: testURL + "/insecure",
|
||||||
AuthURL: "http://example.com/authorize",
|
AuthURL: "http://example.com/authorize",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return testServer
|
return caBundlePEM, testURL
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,9 @@ import (
|
|||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/oidcclient/nonce"
|
|
||||||
"go.pinniped.dev/internal/oidcclient/pkce"
|
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,8 +21,8 @@ import (
|
|||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/oidcclient/nonce"
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||||
"go.pinniped.dev/internal/oidcclient/pkce"
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAuthorizationEndpoint(t *testing.T) {
|
func TestAuthorizationEndpoint(t *testing.T) {
|
||||||
|
@ -16,9 +16,9 @@ import (
|
|||||||
"go.pinniped.dev/internal/oidc/discovery"
|
"go.pinniped.dev/internal/oidc/discovery"
|
||||||
"go.pinniped.dev/internal/oidc/jwks"
|
"go.pinniped.dev/internal/oidc/jwks"
|
||||||
"go.pinniped.dev/internal/oidc/provider"
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/oidcclient/nonce"
|
|
||||||
"go.pinniped.dev/internal/oidcclient/pkce"
|
|
||||||
"go.pinniped.dev/internal/plog"
|
"go.pinniped.dev/internal/plog"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager can manage multiple active OIDC providers. It acts as a request router for them.
|
// Manager can manage multiple active OIDC providers. It acts as a request router for them.
|
||||||
|
@ -16,7 +16,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validSession should be the same data as `testdata/valid.yaml`.
|
// validSession should be the same data as `testdata/valid.yaml`.
|
@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/gofrs/flock"
|
"github.com/gofrs/flock"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
@ -20,9 +20,9 @@ import (
|
|||||||
|
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/httputil/securityheader"
|
"go.pinniped.dev/internal/httputil/securityheader"
|
||||||
"go.pinniped.dev/internal/oidcclient/nonce"
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||||
"go.pinniped.dev/internal/oidcclient/pkce"
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||||
"go.pinniped.dev/internal/oidcclient/state"
|
"go.pinniped.dev/pkg/oidcclient/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -44,6 +44,8 @@ type handlerState struct {
|
|||||||
scopes []string
|
scopes []string
|
||||||
cache SessionCache
|
cache SessionCache
|
||||||
|
|
||||||
|
httpClient *http.Client
|
||||||
|
|
||||||
// Parameters of the localhost listener.
|
// Parameters of the localhost listener.
|
||||||
listenAddr string
|
listenAddr string
|
||||||
callbackPath string
|
callbackPath string
|
||||||
@ -122,6 +124,14 @@ func WithSessionCache(cache SessionCache) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithClient sets the HTTP client used to make CLI-to-provider requests.
|
||||||
|
func WithClient(httpClient *http.Client) Option {
|
||||||
|
return func(h *handlerState) error {
|
||||||
|
h.httpClient = httpClient
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// nopCache is a SessionCache that doesn't actually do anything.
|
// nopCache is a SessionCache that doesn't actually do anything.
|
||||||
type nopCache struct{}
|
type nopCache struct{}
|
||||||
|
|
||||||
@ -144,6 +154,7 @@ func Login(issuer string, clientID string, opts ...Option) (*Token, error) {
|
|||||||
callbackPath: "/callback",
|
callbackPath: "/callback",
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
callbacks: make(chan callbackResult),
|
callbacks: make(chan callbackResult),
|
||||||
|
httpClient: http.DefaultClient,
|
||||||
|
|
||||||
// Default implementations of external dependencies (to be mocked in tests).
|
// Default implementations of external dependencies (to be mocked in tests).
|
||||||
generateState: state.Generate,
|
generateState: state.Generate,
|
||||||
@ -163,6 +174,7 @@ func Login(issuer string, clientID string, opts ...Option) (*Token, error) {
|
|||||||
// Always set a long, but non-infinite timeout for this operation.
|
// Always set a long, but non-infinite timeout for this operation.
|
||||||
ctx, cancel := context.WithTimeout(h.ctx, 10*time.Minute)
|
ctx, cancel := context.WithTimeout(h.ctx, 10*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
ctx = oidc.ClientContext(ctx, h.httpClient)
|
||||||
h.ctx = ctx
|
h.ctx = ctx
|
||||||
|
|
||||||
// Initialize login parameters.
|
// Initialize login parameters.
|
@ -23,9 +23,9 @@ import (
|
|||||||
|
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/mocks/mockkeyset"
|
"go.pinniped.dev/internal/mocks/mockkeyset"
|
||||||
"go.pinniped.dev/internal/oidcclient/nonce"
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||||
"go.pinniped.dev/internal/oidcclient/pkce"
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||||
"go.pinniped.dev/internal/oidcclient/state"
|
"go.pinniped.dev/pkg/oidcclient/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mockSessionCache exists to avoid an import cycle if we generate mocks into another package.
|
// mockSessionCache exists to avoid an import cycle if we generate mocks into another package.
|
||||||
@ -416,6 +416,7 @@ func TestLogin(t *testing.T) {
|
|||||||
require.Equal(t, []*Token{&testToken}, cache.sawPutTokens)
|
require.Equal(t, []*Token{&testToken}, cache.sawPutTokens)
|
||||||
})
|
})
|
||||||
require.NoError(t, WithSessionCache(cache)(h))
|
require.NoError(t, WithSessionCache(cache)(h))
|
||||||
|
require.NoError(t, WithClient(&http.Client{Timeout: 10 * time.Second})(h))
|
||||||
|
|
||||||
h.openURL = func(actualURL string) error {
|
h.openURL = func(actualURL string) error {
|
||||||
parsedActualURL, err := url.Parse(actualURL)
|
parsedActualURL, err := url.Parse(actualURL)
|
@ -12,8 +12,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid two image-right">
|
<div class="grid two image-right">
|
||||||
<div class="col text">
|
<div class="col text">
|
||||||
<p class="strong">Cluster Administration</p>
|
<p class="strong">Seamless Authentication</p>
|
||||||
<p>Easily plug in external IDPs into Kubernetes clusters while offering a simple install and configuration experience. Leverage first class integration with Kubernetes and kubectl CLI.</p>
|
<p>Give users a consistent, unified login experience across all your clusters, including on-premises and managed cloud environments.</p>
|
||||||
<!-- <p><a href="#" class="button tertiary">Read More</a></p> -->
|
<!-- <p><a href="#" class="button tertiary">Read More</a></p> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="col image">
|
<div class="col image">
|
||||||
@ -25,8 +25,8 @@
|
|||||||
<img src="/img/security.svg" />
|
<img src="/img/security.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col text">
|
<div class="col text">
|
||||||
<p class="strong">Cluster Administration</p>
|
<p class="strong">Security for Enterprises & Teams Alike</p>
|
||||||
<p>Easily plug in external IDPs into Kubernetes clusters while offering a simple install and configuration experience. Leverage first class integration with Kubernetes and kubectl CLI.</p>
|
<p>Securely integrate with an enterprise IDP using standard protocols or use secure, externally-managed identities instead of relying on simple, shared credentials.</p>
|
||||||
<!-- <p><a href="#" class="button tertiary">Read More</a></p> -->
|
<!-- <p><a href="#" class="button tertiary">Read More</a></p> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
101
test/deploy/dex/cert-issuer.yaml
Normal file
101
test/deploy/dex/cert-issuer.yaml
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
#! SPDX-License-Identifier: Apache-2.0
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: cert-issuer
|
||||||
|
namespace: dex
|
||||||
|
labels:
|
||||||
|
app: cert-issuer
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: cert-issuer
|
||||||
|
namespace: dex
|
||||||
|
labels:
|
||||||
|
app: cert-issuer
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: [secrets]
|
||||||
|
verbs: [create, get, patch, update, watch, delete]
|
||||||
|
---
|
||||||
|
kind: RoleBinding
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
metadata:
|
||||||
|
name: cert-issuer
|
||||||
|
namespace: dex
|
||||||
|
labels:
|
||||||
|
app: cert-issuer
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: cert-issuer
|
||||||
|
namespace: dex
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: cert-issuer
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
---
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: cert-issuer
|
||||||
|
namespace: dex
|
||||||
|
labels:
|
||||||
|
app: cert-issuer
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
serviceAccountName: cert-issuer
|
||||||
|
initContainers:
|
||||||
|
- name: generate-certs
|
||||||
|
image: cfssl/cfssl:1.5.0
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command: ["/bin/bash"]
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
cd /var/certs
|
||||||
|
cfssl print-defaults config > /tmp/cfssl-default.json
|
||||||
|
echo '{"CN": "Pinniped Test","hosts": [],"key": {"algo": "ecdsa","size": 256},"names": [{}]}' > /tmp/csr.json
|
||||||
|
|
||||||
|
echo "generating CA key..."
|
||||||
|
cfssl genkey \
|
||||||
|
-config /tmp/cfssl-default.json \
|
||||||
|
-initca /tmp/csr.json \
|
||||||
|
| cfssljson -bare ca
|
||||||
|
|
||||||
|
echo "generating Dex server certificate..."
|
||||||
|
cfssl gencert \
|
||||||
|
-ca ca.pem -ca-key ca-key.pem \
|
||||||
|
-config /tmp/cfssl-default.json \
|
||||||
|
-profile www \
|
||||||
|
-cn "dex.dex.svc.cluster.local" \
|
||||||
|
-hostname "dex.dex.svc.cluster.local" \
|
||||||
|
/tmp/csr.json \
|
||||||
|
| cfssljson -bare dex
|
||||||
|
|
||||||
|
chmod -R 777 /var/certs
|
||||||
|
|
||||||
|
echo "generated certificates:"
|
||||||
|
ls -l /var/certs
|
||||||
|
volumeMounts:
|
||||||
|
- name: certs
|
||||||
|
mountPath: /var/certs
|
||||||
|
containers:
|
||||||
|
- name: save-certs
|
||||||
|
image: bitnami/kubectl
|
||||||
|
command: ["/bin/bash"]
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
kubectl get secrets -n dex certs -o jsonpath='created: {.metadata.creationTimestamp}' || \
|
||||||
|
kubectl create secret generic certs --from-file=/var/certs
|
||||||
|
volumeMounts:
|
||||||
|
- name: certs
|
||||||
|
mountPath: /var/certs
|
||||||
|
volumes:
|
||||||
|
- name: certs
|
||||||
|
emptyDir: {}
|
||||||
|
restartPolicy: Never
|
@ -6,13 +6,15 @@
|
|||||||
#@ load("@ytt:yaml", "yaml")
|
#@ load("@ytt:yaml", "yaml")
|
||||||
|
|
||||||
#@ def dexConfig():
|
#@ def dexConfig():
|
||||||
issuer: #@ "http://127.0.0.1:" + str(data.values.ports.local) + "/dex"
|
issuer: https://dex.dex.svc.cluster.local/dex
|
||||||
storage:
|
storage:
|
||||||
type: sqlite3
|
type: sqlite3
|
||||||
config:
|
config:
|
||||||
file: ":memory:"
|
file: ":memory:"
|
||||||
web:
|
web:
|
||||||
http: 0.0.0.0:5556
|
https: 0.0.0.0:443
|
||||||
|
tlsCert: /var/certs/dex.pem
|
||||||
|
tlsKey: /var/certs/dex-key.pem
|
||||||
oauth2:
|
oauth2:
|
||||||
skipApprovalScreen: true
|
skipApprovalScreen: true
|
||||||
staticClients:
|
staticClients:
|
||||||
@ -68,23 +70,29 @@ spec:
|
|||||||
dexConfigHash: #@ sha256.sum(yaml.encode(dexConfig()))
|
dexConfigHash: #@ sha256.sum(yaml.encode(dexConfig()))
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: dex
|
- name: dex
|
||||||
image: quay.io/dexidp/dex:v2.10.0
|
image: quay.io/dexidp/dex:v2.10.0
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /usr/local/bin/dex
|
- /usr/local/bin/dex
|
||||||
- serve
|
- serve
|
||||||
- /etc/dex/cfg/config.yaml
|
- /etc/dex/cfg/config.yaml
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: https
|
||||||
containerPort: 5556
|
containerPort: 443
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config
|
- name: dex-config
|
||||||
mountPath: /etc/dex/cfg
|
mountPath: /etc/dex/cfg
|
||||||
|
- name: certs
|
||||||
|
mountPath: /var/certs
|
||||||
|
readOnly: true
|
||||||
volumes:
|
volumes:
|
||||||
- name: config
|
- name: dex-config
|
||||||
configMap:
|
configMap:
|
||||||
name: dex-config
|
name: dex-config
|
||||||
|
- name: certs
|
||||||
|
secret:
|
||||||
|
secretName: certs
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
@ -94,9 +102,9 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
app: dex
|
app: dex
|
||||||
spec:
|
spec:
|
||||||
type: NodePort
|
type: ClusterIP
|
||||||
selector:
|
selector:
|
||||||
app: dex
|
app: dex
|
||||||
ports:
|
ports:
|
||||||
- port: 5556
|
- port: 443
|
||||||
nodePort: #@ data.values.ports.node
|
name: https
|
||||||
|
58
test/deploy/dex/proxy.yaml
Normal file
58
test/deploy/dex/proxy.yaml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
#! SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
#@ load("@ytt:data", "data")
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: proxy
|
||||||
|
namespace: dex
|
||||||
|
labels:
|
||||||
|
app: proxy
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: proxy
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: proxy
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: proxy
|
||||||
|
image: docker.io/getpinniped/test-forward-proxy
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3128
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "10m"
|
||||||
|
memory: "64Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "10m"
|
||||||
|
memory: "64Mi"
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 2
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: proxy
|
||||||
|
namespace: dex
|
||||||
|
labels:
|
||||||
|
app: proxy
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: proxy
|
||||||
|
ports:
|
||||||
|
- port: 3128
|
||||||
|
nodePort: #@ data.values.ports.node
|
@ -8,10 +8,10 @@ ports:
|
|||||||
#! Used in the Dex configuration to form the valid redirect URIs for our test client.
|
#! Used in the Dex configuration to form the valid redirect URIs for our test client.
|
||||||
cli: 48095
|
cli: 48095
|
||||||
|
|
||||||
#! Kubernetes NodePort that should be forwarded to the Dex service.
|
#! Kubernetes NodePort that should be forwarded to the proxy service.
|
||||||
#! Used to create a Service of type: NodePort
|
#! Used to create a Service of type: NodePort
|
||||||
node: 31235
|
node: 31235
|
||||||
|
|
||||||
#! External port where Dex ends up exposed on localhost during tests. This value comes from our
|
#! External port where the proxy ends up exposed on localhost during tests. This value comes from
|
||||||
#! Kind configuration which maps 127.0.0.1:12346 to port 31235 on the Kind worker node.
|
#! our Kind configuration which maps 127.0.0.1:12346 to port 31235 on the Kind worker node.
|
||||||
local: 12346
|
local: 12346
|
||||||
|
@ -26,8 +26,8 @@ import (
|
|||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidcclient"
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
"go.pinniped.dev/internal/oidcclient/filesession"
|
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||||
"go.pinniped.dev/test/library"
|
"go.pinniped.dev/test/library"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -130,8 +130,8 @@ func getLoginProvider(t *testing.T) *loginProviderPatterns {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Dex",
|
Name: "Dex",
|
||||||
IssuerPattern: regexp.MustCompile(`\Ahttp://127\.0\.0\.1.+/dex.*\z`),
|
IssuerPattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex.*\z`),
|
||||||
LoginPagePattern: regexp.MustCompile(`\Ahttp://127\.0\.0\.1.+/dex/auth/local.+\z`),
|
LoginPagePattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex/auth/local.+\z`),
|
||||||
UsernameSelector: "input#login",
|
UsernameSelector: "input#login",
|
||||||
PasswordSelector: "input#password",
|
PasswordSelector: "input#password",
|
||||||
LoginButtonSelector: "button#submit-login",
|
LoginButtonSelector: "button#submit-login",
|
||||||
@ -156,9 +156,21 @@ func TestCLILoginOIDC(t *testing.T) {
|
|||||||
|
|
||||||
// Start the browser driver.
|
// Start the browser driver.
|
||||||
t.Logf("opening browser driver")
|
t.Logf("opening browser driver")
|
||||||
|
caps := agouti.NewCapabilities()
|
||||||
|
if env.Proxy != "" {
|
||||||
|
t.Logf("configuring Chrome to use proxy %q", env.Proxy)
|
||||||
|
caps = caps.Proxy(agouti.ProxyConfig{
|
||||||
|
ProxyType: "manual",
|
||||||
|
HTTPProxy: env.Proxy,
|
||||||
|
SSLProxy: env.Proxy,
|
||||||
|
NoProxy: "127.0.0.1",
|
||||||
|
})
|
||||||
|
}
|
||||||
agoutiDriver := agouti.ChromeDriver(
|
agoutiDriver := agouti.ChromeDriver(
|
||||||
|
agouti.Desired(caps),
|
||||||
agouti.ChromeOptions("args", []string{
|
agouti.ChromeOptions("args", []string{
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
|
"--ignore-certificate-errors",
|
||||||
"--headless", // Comment out this line to see the tests happen in a visible browser window.
|
"--headless", // Comment out this line to see the tests happen in a visible browser window.
|
||||||
}),
|
}),
|
||||||
// Uncomment this to see stdout/stderr from chromedriver.
|
// Uncomment this to see stdout/stderr from chromedriver.
|
||||||
@ -395,11 +407,28 @@ func spawnTestGoroutine(t *testing.T, f func() error) {
|
|||||||
|
|
||||||
func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, sessionCachePath string) *exec.Cmd {
|
func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, sessionCachePath string) *exec.Cmd {
|
||||||
env := library.IntegrationEnv(t)
|
env := library.IntegrationEnv(t)
|
||||||
return exec.CommandContext(ctx, pinnipedExe, "login", "oidc",
|
cmd := exec.CommandContext(ctx, pinnipedExe, "login", "oidc",
|
||||||
"--issuer", env.OIDCUpstream.Issuer,
|
"--issuer", env.OIDCUpstream.Issuer,
|
||||||
"--client-id", env.OIDCUpstream.ClientID,
|
"--client-id", env.OIDCUpstream.ClientID,
|
||||||
"--listen-port", strconv.Itoa(env.OIDCUpstream.LocalhostPort),
|
"--listen-port", strconv.Itoa(env.OIDCUpstream.LocalhostPort),
|
||||||
"--session-cache", sessionCachePath,
|
"--session-cache", sessionCachePath,
|
||||||
"--skip-browser",
|
"--skip-browser",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If there is a custom CA bundle, pass it via --ca-bundle and a temporary file.
|
||||||
|
if env.OIDCUpstream.CABundle != "" {
|
||||||
|
path := filepath.Join(t.TempDir(), "test-ca.pem")
|
||||||
|
require.NoError(t, ioutil.WriteFile(path, []byte(env.OIDCUpstream.CABundle), 0600))
|
||||||
|
cmd.Args = append(cmd.Args, "--ca-bundle", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a custom proxy, set it using standard environment variables.
|
||||||
|
if env.Proxy != "" {
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
"http_proxy="+env.Proxy,
|
||||||
|
"https_proxy="+env.Proxy,
|
||||||
|
"no_proxy=127.0.0.1",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
196
test/integration/supervisor_login_test.go
Normal file
196
test/integration/supervisor_login_test.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/state"
|
||||||
|
"go.pinniped.dev/test/library"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSupervisorLogin(t *testing.T) {
|
||||||
|
env := library.IntegrationEnv(t)
|
||||||
|
client := library.NewSupervisorClientset(t)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Create downstream OIDC provider (i.e., update supervisor with OIDC provider).
|
||||||
|
scheme := "http"
|
||||||
|
addr := env.SupervisorHTTPAddress
|
||||||
|
caBundle := ""
|
||||||
|
path := "/some/path"
|
||||||
|
issuer := fmt.Sprintf("https://%s%s", addr, path)
|
||||||
|
_, _ = requireCreatingOIDCProviderCausesDiscoveryEndpointsToAppear(
|
||||||
|
ctx,
|
||||||
|
t,
|
||||||
|
scheme,
|
||||||
|
addr,
|
||||||
|
caBundle,
|
||||||
|
issuer,
|
||||||
|
client,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create HTTP client.
|
||||||
|
httpClient := newHTTPClient(t, caBundle, nil)
|
||||||
|
httpClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
||||||
|
// Don't follow any redirects right now, since we simply want to validate that our auth endpoint
|
||||||
|
// redirects us.
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare the downstream auth endpoint url we will use.
|
||||||
|
downstreamAuthURL := makeDownstreamAuthURL(t, scheme, addr, path)
|
||||||
|
|
||||||
|
// Make request to auth endpoint - should fail, since we have no upstreams.
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthURL, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rsp, err := httpClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
require.Equal(t, http.StatusUnprocessableEntity, rsp.StatusCode)
|
||||||
|
|
||||||
|
// Create upstream OIDC provider.
|
||||||
|
testClientID := "test-client-id"
|
||||||
|
testClientSecret := "test-client-secret"
|
||||||
|
spec := idpv1alpha1.UpstreamOIDCProviderSpec{
|
||||||
|
Issuer: env.OIDCUpstream.Issuer,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.OIDCUpstream.CABundle)),
|
||||||
|
},
|
||||||
|
AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{
|
||||||
|
AdditionalScopes: []string{},
|
||||||
|
},
|
||||||
|
Client: idpv1alpha1.OIDCClient{
|
||||||
|
SecretName: makeTestClientCredsSecret(t, testClientID, testClientSecret).Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
upstream := makeTestUpstream(t, spec, idpv1alpha1.PhaseReady)
|
||||||
|
|
||||||
|
upstreamRedirectURI := fmt.Sprintf("https://%s/some/path/callback/%s", env.SupervisorHTTPAddress, upstream.Name)
|
||||||
|
|
||||||
|
// Make request to authorize endpoint - should pass, since we now have an upstream.
|
||||||
|
req, err = http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthURL, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rsp, err = httpClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
require.Equal(t, http.StatusFound, rsp.StatusCode)
|
||||||
|
requireValidRedirectLocation(
|
||||||
|
ctx,
|
||||||
|
t,
|
||||||
|
upstream.Spec.Issuer,
|
||||||
|
testClientID,
|
||||||
|
upstreamRedirectURI,
|
||||||
|
rsp.Header.Get("Location"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeDownstreamAuthURL(t *testing.T, scheme, addr, path string) string {
|
||||||
|
t.Helper()
|
||||||
|
downstreamOAuth2Config := oauth2.Config{
|
||||||
|
// This is the hardcoded public client that the supervisor supports.
|
||||||
|
ClientID: "pinniped-cli",
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: fmt.Sprintf("%s://%s%s/oauth2/authorize", scheme, addr, path),
|
||||||
|
},
|
||||||
|
// This is the hardcoded downstream redirect URI that the supervisor supports.
|
||||||
|
RedirectURL: "http://127.0.0.1/callback",
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
}
|
||||||
|
state, nonce, pkce := generateAuthRequestParams(t)
|
||||||
|
return downstreamOAuth2Config.AuthCodeURL(
|
||||||
|
state.String(),
|
||||||
|
nonce.Param(),
|
||||||
|
pkce.Challenge(),
|
||||||
|
pkce.Method(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateAuthRequestParams(t *testing.T) (state.State, nonce.Nonce, pkce.Code) {
|
||||||
|
t.Helper()
|
||||||
|
state, err := state.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
nonce, err := nonce.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
pkce, err := pkce.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
return state, nonce, pkce
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireValidRedirectLocation(
|
||||||
|
ctx context.Context,
|
||||||
|
t *testing.T,
|
||||||
|
issuer, clientID, redirectURI, actualLocation string,
|
||||||
|
) {
|
||||||
|
t.Helper()
|
||||||
|
env := library.IntegrationEnv(t)
|
||||||
|
|
||||||
|
// Do OIDC discovery on our test issuer to get auth endpoint.
|
||||||
|
transport := http.Transport{}
|
||||||
|
if env.Proxy != "" {
|
||||||
|
transport.Proxy = func(_ *http.Request) (*url.URL, error) {
|
||||||
|
return url.Parse(env.Proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if env.OIDCUpstream.CABundle != "" {
|
||||||
|
transport.TLSClientConfig = &tls.Config{RootCAs: x509.NewCertPool()}
|
||||||
|
transport.TLSClientConfig.RootCAs.AppendCertsFromPEM([]byte(env.OIDCUpstream.CABundle))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = oidc.ClientContext(ctx, &http.Client{Transport: &transport})
|
||||||
|
upstreamProvider, err := oidc.NewProvider(ctx, issuer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Parse expected upstream auth URL.
|
||||||
|
expectedLocationURL, err := url.Parse(
|
||||||
|
(&oauth2.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
Endpoint: upstreamProvider.Endpoint(),
|
||||||
|
RedirectURL: redirectURI,
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
}).AuthCodeURL("", oauth2.AccessTypeOffline),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Parse actual upstream auth URL.
|
||||||
|
actualLocationURL, err := url.Parse(actualLocation)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// First make some assertions on the query values. Note that we will not be able to know what
|
||||||
|
// certain query values are since they may be random (e.g., state, pkce, nonce).
|
||||||
|
expectedLocationQuery := expectedLocationURL.Query()
|
||||||
|
actualLocationQuery := actualLocationURL.Query()
|
||||||
|
require.NotEmpty(t, actualLocationQuery.Get("state"))
|
||||||
|
actualLocationQuery.Del("state")
|
||||||
|
require.NotEmpty(t, actualLocationQuery.Get("code_challenge"))
|
||||||
|
actualLocationQuery.Del("code_challenge")
|
||||||
|
require.NotEmpty(t, actualLocationQuery.Get("code_challenge_method"))
|
||||||
|
actualLocationQuery.Del("code_challenge_method")
|
||||||
|
require.NotEmpty(t, actualLocationQuery.Get("nonce"))
|
||||||
|
actualLocationQuery.Del("nonce")
|
||||||
|
require.Equal(t, expectedLocationQuery, actualLocationQuery)
|
||||||
|
|
||||||
|
// Zero-out query values, since we made specific assertions about those above, and assert that the
|
||||||
|
// URL's are equal otherwise.
|
||||||
|
expectedLocationURL.RawQuery = ""
|
||||||
|
actualLocationURL.RawQuery = ""
|
||||||
|
require.Equal(t, expectedLocationURL, actualLocationURL)
|
||||||
|
}
|
@ -5,6 +5,7 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
|
func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
|
||||||
library.SkipUnlessIntegration(t)
|
env := library.IntegrationEnv(t)
|
||||||
|
|
||||||
t.Run("invalid missing secret and bad issuer", func(t *testing.T) {
|
t.Run("invalid missing secret and bad issuer", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@ -50,7 +51,10 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
|
|||||||
t.Run("valid", func(t *testing.T) {
|
t.Run("valid", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
spec := v1alpha1.UpstreamOIDCProviderSpec{
|
spec := v1alpha1.UpstreamOIDCProviderSpec{
|
||||||
Issuer: "https://accounts.google.com", // Use Google as an example of a valid OIDC issuer for now.
|
Issuer: env.OIDCUpstream.Issuer,
|
||||||
|
TLS: &v1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.OIDCUpstream.CABundle)),
|
||||||
|
},
|
||||||
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
|
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
|
||||||
AdditionalScopes: []string{"email", "profile"},
|
AdditionalScopes: []string{"email", "profile"},
|
||||||
},
|
},
|
||||||
|
@ -38,6 +38,7 @@ type TestEnv struct {
|
|||||||
SupervisorHTTPSAddress string `json:"supervisorHttpsAddress"`
|
SupervisorHTTPSAddress string `json:"supervisorHttpsAddress"`
|
||||||
SupervisorHTTPSIngressAddress string `json:"supervisorHttpsIngressAddress"`
|
SupervisorHTTPSIngressAddress string `json:"supervisorHttpsIngressAddress"`
|
||||||
SupervisorHTTPSIngressCABundle string `json:"supervisorHttpsIngressCABundle"`
|
SupervisorHTTPSIngressCABundle string `json:"supervisorHttpsIngressCABundle"`
|
||||||
|
Proxy string `json:"proxy"`
|
||||||
|
|
||||||
TestUser struct {
|
TestUser struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
@ -47,6 +48,7 @@ type TestEnv struct {
|
|||||||
|
|
||||||
OIDCUpstream struct {
|
OIDCUpstream struct {
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
|
CABundle string `json:"caBundle" `
|
||||||
ClientID string `json:"clientID"`
|
ClientID string `json:"clientID"`
|
||||||
LocalhostPort int `json:"localhostPort"`
|
LocalhostPort int `json:"localhostPort"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@ -126,8 +128,10 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
|
|||||||
require.NoErrorf(t, err, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS must be a YAML map of string to string")
|
require.NoErrorf(t, err, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS must be a YAML map of string to string")
|
||||||
result.SupervisorCustomLabels = supervisorCustomLabels
|
result.SupervisorCustomLabels = supervisorCustomLabels
|
||||||
require.NotEmpty(t, result.SupervisorCustomLabels, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS cannot be empty")
|
require.NotEmpty(t, result.SupervisorCustomLabels, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS cannot be empty")
|
||||||
|
result.Proxy = os.Getenv("PINNIPED_TEST_PROXY")
|
||||||
|
|
||||||
result.OIDCUpstream.Issuer = needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER")
|
result.OIDCUpstream.Issuer = needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER")
|
||||||
|
result.OIDCUpstream.CABundle = os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE")
|
||||||
result.OIDCUpstream.ClientID = needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID")
|
result.OIDCUpstream.ClientID = needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID")
|
||||||
result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv(t, "PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT"))
|
result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv(t, "PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT"))
|
||||||
result.OIDCUpstream.Username = needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME")
|
result.OIDCUpstream.Username = needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME")
|
||||||
|
Loading…
Reference in New Issue
Block a user