mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-07 05:57:02 +00:00
Pinniped CLI and the oidc-client package are now enhanced by pinniped_supported_identity_provider_types
Co-authored-by: Joshua Casey <joshuatcasey@gmail.com>
This commit is contained in:
committed by
Joshua Casey
parent
a86d7d27c1
commit
7e0a3c114d
@@ -908,7 +908,7 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
idpsDiscoveryResponse: here.Docf(`{
|
||||
"pinniped_identity_providers": [
|
||||
{"name": "some-ldap-idp", "type": "ldap"},
|
||||
{"name": "some-oidc-idp", "type": "oidc"}
|
||||
{"name": "some-oidc-idp", "type": "oidc", "flows": ["flow1", "flow2"]}
|
||||
]
|
||||
}`),
|
||||
wantLogs: func(issuerCABundle string, issuerURL string) []string {
|
||||
@@ -927,7 +927,7 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc {
|
||||
return testutil.WantExactErrorString(`Error: multiple Supervisor upstream identity providers were found, ` +
|
||||
`so the --upstream-identity-provider-name/--upstream-identity-provider-type flags must be specified. ` +
|
||||
`Found these upstreams: [{"name":"some-ldap-idp","type":"ldap"},{"name":"some-oidc-idp","type":"oidc"}]` + "\n")
|
||||
`Found these upstreams: [{"name":"some-ldap-idp","type":"ldap"},{"name":"some-oidc-idp","type":"oidc","flows":["flow1","flow2"]}]` + "\n")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -59,9 +58,10 @@ func init() {
|
||||
}
|
||||
|
||||
type oidcLoginCommandDeps struct {
|
||||
lookupEnv func(string) (string, bool)
|
||||
login func(string, string, ...oidcclient.Option) (*oidctypes.Token, error)
|
||||
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
|
||||
lookupEnv func(string) (string, bool)
|
||||
login func(string, string, ...oidcclient.Option) (*oidctypes.Token, error)
|
||||
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
|
||||
optionsFactory OIDCClientOptions
|
||||
}
|
||||
|
||||
func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
|
||||
@@ -71,6 +71,7 @@ func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
|
||||
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
|
||||
return client.ExchangeToken(ctx, token)
|
||||
},
|
||||
optionsFactory: &clientOptions{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,39 +176,37 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
|
||||
// Initialize the login handler.
|
||||
opts := []oidcclient.Option{
|
||||
oidcclient.WithContext(cmd.Context()),
|
||||
oidcclient.WithLogger(plog.Logr()), //nolint:staticcheck // old code with lots of log statements
|
||||
oidcclient.WithScopes(flags.scopes),
|
||||
oidcclient.WithSessionCache(sessionCache),
|
||||
deps.optionsFactory.WithContext(cmd.Context()),
|
||||
deps.optionsFactory.WithLogger(plog.Logr()), //nolint:staticcheck // old code with lots of log statements
|
||||
deps.optionsFactory.WithScopes(flags.scopes),
|
||||
deps.optionsFactory.WithSessionCache(sessionCache),
|
||||
}
|
||||
|
||||
skipPrintLoginURL, _ := deps.lookupEnv(skipPrintLoginURLEnvVarName)
|
||||
if skipPrintLoginURL == envVarTruthyValue {
|
||||
opts = append(opts, oidcclient.WithSkipPrintLoginURL())
|
||||
opts = append(opts, deps.optionsFactory.WithSkipPrintLoginURL())
|
||||
}
|
||||
|
||||
if flags.listenPort != 0 {
|
||||
opts = append(opts, oidcclient.WithListenPort(flags.listenPort))
|
||||
opts = append(opts, deps.optionsFactory.WithListenPort(flags.listenPort))
|
||||
}
|
||||
|
||||
if flags.requestAudience != "" {
|
||||
opts = append(opts, oidcclient.WithRequestAudience(flags.requestAudience))
|
||||
opts = append(opts, deps.optionsFactory.WithRequestAudience(flags.requestAudience))
|
||||
}
|
||||
|
||||
if flags.upstreamIdentityProviderName != "" {
|
||||
opts = append(opts, oidcclient.WithUpstreamIdentityProvider(
|
||||
opts = append(opts, deps.optionsFactory.WithUpstreamIdentityProvider(
|
||||
flags.upstreamIdentityProviderName, flags.upstreamIdentityProviderType))
|
||||
}
|
||||
|
||||
flowOpts, err := flowOptions(
|
||||
idpdiscoveryv1alpha1.IDPType(flags.upstreamIdentityProviderType),
|
||||
idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow),
|
||||
deps,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
requestedFlow, flowSource := idpdiscoveryv1alpha1.IDPFlow(flags.upstreamIdentityProviderFlow), "--upstream-identity-provider-flow"
|
||||
if flowOverride, hasFlowOverride := deps.lookupEnv(upstreamIdentityProviderFlowEnvVarName); hasFlowOverride {
|
||||
requestedFlow, flowSource = idpdiscoveryv1alpha1.IDPFlow(flowOverride), upstreamIdentityProviderFlowEnvVarName
|
||||
}
|
||||
if requestedFlow != "" {
|
||||
opts = append(opts, deps.optionsFactory.WithLoginFlow(requestedFlow, flowSource))
|
||||
}
|
||||
opts = append(opts, flowOpts...)
|
||||
|
||||
var concierge *conciergeclient.Client
|
||||
if flags.conciergeEnabled {
|
||||
@@ -225,12 +224,12 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
|
||||
// --skip-browser skips opening the browser.
|
||||
if flags.skipBrowser {
|
||||
opts = append(opts, oidcclient.WithSkipBrowserOpen())
|
||||
opts = append(opts, deps.optionsFactory.WithSkipBrowserOpen())
|
||||
}
|
||||
|
||||
// --skip-listen skips starting the localhost callback listener.
|
||||
if flags.skipListen {
|
||||
opts = append(opts, oidcclient.WithSkipListen())
|
||||
opts = append(opts, deps.optionsFactory.WithSkipListen())
|
||||
}
|
||||
|
||||
if len(flags.caBundlePaths) > 0 || len(flags.caBundleData) > 0 {
|
||||
@@ -238,7 +237,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts = append(opts, oidcclient.WithClient(client))
|
||||
opts = append(opts, deps.optionsFactory.WithClient(client))
|
||||
}
|
||||
// Look up cached credentials based on a hash of all the CLI arguments and the cluster info.
|
||||
cacheKey := struct {
|
||||
@@ -288,60 +287,6 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
|
||||
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
|
||||
}
|
||||
|
||||
func flowOptions(
|
||||
requestedIDPType idpdiscoveryv1alpha1.IDPType,
|
||||
requestedFlow idpdiscoveryv1alpha1.IDPFlow,
|
||||
deps oidcLoginCommandDeps,
|
||||
) ([]oidcclient.Option, error) {
|
||||
useCLIFlow := []oidcclient.Option{oidcclient.WithCLISendingCredentials()}
|
||||
|
||||
// If the env var is set to override the --upstream-identity-provider-type flag, then override it.
|
||||
flowOverride, hasFlowOverride := deps.lookupEnv(upstreamIdentityProviderFlowEnvVarName)
|
||||
flowSource := "--upstream-identity-provider-flow"
|
||||
if hasFlowOverride {
|
||||
requestedFlow = idpdiscoveryv1alpha1.IDPFlow(flowOverride)
|
||||
flowSource = upstreamIdentityProviderFlowEnvVarName
|
||||
}
|
||||
|
||||
switch requestedIDPType {
|
||||
case idpdiscoveryv1alpha1.IDPTypeOIDC:
|
||||
switch requestedFlow {
|
||||
case idpdiscoveryv1alpha1.IDPFlowCLIPassword:
|
||||
return useCLIFlow, nil
|
||||
case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode, "":
|
||||
return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"%s value not recognized for identity provider type %q: %s (supported values: %s)",
|
||||
flowSource, requestedIDPType, requestedFlow,
|
||||
strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String(), idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}, ", "))
|
||||
}
|
||||
case idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory:
|
||||
switch requestedFlow {
|
||||
case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "":
|
||||
return useCLIFlow, nil
|
||||
case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode:
|
||||
return nil, nil // browser authcode flow is the default Option, so don't need to return an Option here
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"%s value not recognized for identity provider type %q: %s (supported values: %s)",
|
||||
flowSource, requestedIDPType, requestedFlow,
|
||||
strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String(), idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String()}, ", "))
|
||||
}
|
||||
default:
|
||||
// Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236
|
||||
return nil, fmt.Errorf(
|
||||
"--upstream-identity-provider-type value not recognized: %s (supported values: %s)",
|
||||
requestedIDPType,
|
||||
strings.Join([]string{
|
||||
idpdiscoveryv1alpha1.IDPTypeOIDC.String(),
|
||||
idpdiscoveryv1alpha1.IDPTypeLDAP.String(),
|
||||
idpdiscoveryv1alpha1.IDPTypeActiveDirectory.String(),
|
||||
}, ", "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, error) {
|
||||
pool := x509.NewCertPool()
|
||||
for _, p := range caBundlePaths {
|
||||
|
||||
@@ -14,12 +14,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
clocktesting "k8s.io/utils/clock/testing"
|
||||
|
||||
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/mocks/mockoidcclientoptions"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/pkg/conciergeclient"
|
||||
@@ -42,6 +46,13 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
nowStr := now.Local().Format(time.RFC1123)
|
||||
|
||||
defaultWantedOptions := func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
f.EXPECT().WithContext(gomock.Any())
|
||||
f.EXPECT().WithLogger(gomock.Any())
|
||||
f.EXPECT().WithScopes([]string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups})
|
||||
f.EXPECT().WithSessionCache(gomock.Any())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
@@ -51,6 +62,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
wantError bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
wantOptions func(f *mockoidcclientoptions.MockOIDCClientOptions)
|
||||
wantOptionsCount int
|
||||
wantLogs []string
|
||||
}{
|
||||
@@ -109,7 +121,8 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--issuer", "test-issuer",
|
||||
"--enable-concierge",
|
||||
},
|
||||
wantError: true,
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: endpoint must not be empty
|
||||
`),
|
||||
@@ -121,7 +134,8 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--issuer", "test-issuer",
|
||||
"--ca-bundle", "./does/not/exist",
|
||||
},
|
||||
wantError: true,
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not read --ca-bundle: open ./does/not/exist: no such file or directory
|
||||
`),
|
||||
@@ -133,7 +147,8 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--issuer", "test-issuer",
|
||||
"--ca-bundle-data", "invalid-base64",
|
||||
},
|
||||
wantError: true,
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: could not read --ca-bundle-data: illegal base64 data at input byte 7
|
||||
`),
|
||||
@@ -148,34 +163,12 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--concierge-authenticator-name", "test-authenticator",
|
||||
"--concierge-endpoint", "https://127.0.0.1:1234/",
|
||||
},
|
||||
wantError: true,
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: invalid Concierge parameters: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid upstream type is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--upstream-identity-provider-type", "invalid",
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "invalid upstream type when flow override env var is used is still an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--upstream-identity-provider-type", "invalid",
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with default flow is allowed",
|
||||
args: []string{
|
||||
@@ -184,6 +177,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
@@ -195,269 +189,45 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
|
||||
env: map[string]string{"PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
defaultWantedOptions(f)
|
||||
f.EXPECT().WithSkipPrintLoginURL()
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with CLI flow is allowed",
|
||||
name: "--upstream-identity-provider-flow adds an option",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with browser flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with CLI flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with with browser flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with unsupported flow is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "foobar",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-flow value not recognized for identity provider type "oidc": foobar (supported values: browser_authcode, cli_password)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "oidc upstream type with unsupported flow in flow override env var is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "oidc",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "oidc": foo (supported values: browser_authcode, cli_password)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with default flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
defaultWantedOptions(f)
|
||||
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlowCLIPassword, "--upstream-identity-provider-flow")
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "activedirectory upstream type with default flow is allowed",
|
||||
name: "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW adds an option that overrides --upstream-identity-provider-flow",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "ignored-value-from-param",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "actual-value-from-env"},
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
defaultWantedOptions(f)
|
||||
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlow("actual-value-from-env"), "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW")
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with CLI flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with browser_authcode flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with CLI flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with browser_authcode flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with unsupported flow is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "foo",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "ldap upstream type with unsupported flow in flow override env var is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with CLI flow is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with browser_authcode is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with CLI flow in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "cli_password"},
|
||||
wantOptionsCount: 5,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with browser_authcode in flow override env var is allowed",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "cli_password",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "browser_authcode"},
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with unsupported flow is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "foo",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "active directory upstream type with unsupported flow in flow override env var is an error",
|
||||
args: []string{
|
||||
"--issuer", "test-issuer",
|
||||
"--client-id", "test-client-id",
|
||||
"--upstream-identity-provider-type", "activedirectory",
|
||||
"--upstream-identity-provider-flow", "browser_authcode",
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW": "foo"},
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
Error: PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode)
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "login error",
|
||||
args: []string{
|
||||
@@ -466,6 +236,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
loginErr: fmt.Errorf("some login error"),
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
@@ -484,6 +255,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
conciergeErr: fmt.Errorf("some concierge error"),
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantError: true,
|
||||
wantStderr: here.Doc(`
|
||||
@@ -498,11 +270,12 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true"},
|
||||
wantOptions: defaultWantedOptions,
|
||||
wantOptionsCount: 4,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
|
||||
wantLogs: []string{
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:260 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:280 No concierge configured, skipping token credential exchange`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:259 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:279 No concierge configured, skipping token credential exchange`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -526,15 +299,30 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
"--credential-cache", t.TempDir() + "/credentials.yaml", // must specify --credential-cache or else the cache file on disk causes test pollution
|
||||
"--upstream-identity-provider-name", "some-upstream-name",
|
||||
"--upstream-identity-provider-type", "ldap",
|
||||
"--upstream-identity-provider-flow", "some-flow-type",
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true", "PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
|
||||
wantOptions: func(f *mockoidcclientoptions.MockOIDCClientOptions) {
|
||||
f.EXPECT().WithContext(gomock.Any())
|
||||
f.EXPECT().WithLogger(gomock.Any())
|
||||
f.EXPECT().WithScopes([]string{oidcapi.ScopeOfflineAccess, oidcapi.ScopeOpenID, oidcapi.ScopeRequestAudience, oidcapi.ScopeUsername, oidcapi.ScopeGroups})
|
||||
f.EXPECT().WithSessionCache(gomock.Any())
|
||||
f.EXPECT().WithListenPort(uint16(1234))
|
||||
f.EXPECT().WithSkipBrowserOpen()
|
||||
f.EXPECT().WithSkipListen()
|
||||
f.EXPECT().WithSkipPrintLoginURL()
|
||||
f.EXPECT().WithClient(gomock.Any())
|
||||
f.EXPECT().WithRequestAudience("cluster-1234")
|
||||
f.EXPECT().WithLoginFlow(idpdiscoveryv1alpha1.IDPFlow("some-flow-type"), "--upstream-identity-provider-flow")
|
||||
f.EXPECT().WithUpstreamIdentityProvider("some-upstream-name", "ldap")
|
||||
},
|
||||
env: map[string]string{"PINNIPED_DEBUG": "true", "PINNIPED_SKIP_PRINT_LOGIN_URL": "true"},
|
||||
wantOptionsCount: 12,
|
||||
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n",
|
||||
wantLogs: []string{
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:260 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:270 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:278 Successfully exchanged token for cluster credential.`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:285 caching cluster credential for future use.`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:259 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:269 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:277 Successfully exchanged token for cluster credential.`,
|
||||
nowStr + ` pinniped-login cmd/login_oidc.go:284 caching cluster credential for future use.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -543,6 +331,13 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
ctx := plog.AddZapOverridesToContext(context.Background(), t, &buf, nil, clocktesting.NewFakeClock(now))
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
t.Cleanup(ctrl.Finish)
|
||||
optionsFactory := mockoidcclientoptions.NewMockOIDCClientOptions(ctrl)
|
||||
if tt.wantOptions != nil {
|
||||
tt.wantOptions(optionsFactory)
|
||||
}
|
||||
|
||||
var gotOptions []oidcclient.Option
|
||||
cmd := oidcLoginCommand(oidcLoginCommandDeps{
|
||||
lookupEnv: func(s string) (string, bool) {
|
||||
@@ -578,6 +373,7 @@ func TestLoginOIDCCommand(t *testing.T) {
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
optionsFactory: optionsFactory,
|
||||
})
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
|
||||
85
cmd/pinniped/cmd/oidc_client_options.go
Normal file
85
cmd/pinniped/cmd/oidc_client_options.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
||||
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
)
|
||||
|
||||
// OIDCClientOptions is an interface that wraps the creation of Options for the purpose of making them
|
||||
// more friendly to unit tests. Because the Option type refers to a private struct type, it is hard
|
||||
// to create mocks for them in tests of other packages. This provides a seam that can be mocked.
|
||||
type OIDCClientOptions interface {
|
||||
WithContext(ctx context.Context) oidcclient.Option
|
||||
WithLogger(logger logr.Logger) oidcclient.Option
|
||||
WithListenPort(port uint16) oidcclient.Option
|
||||
WithSkipBrowserOpen() oidcclient.Option
|
||||
WithSkipListen() oidcclient.Option
|
||||
WithSkipPrintLoginURL() oidcclient.Option
|
||||
WithSessionCache(cache oidcclient.SessionCache) oidcclient.Option
|
||||
WithClient(httpClient *http.Client) oidcclient.Option
|
||||
WithScopes(scopes []string) oidcclient.Option
|
||||
WithRequestAudience(audience string) oidcclient.Option
|
||||
WithLoginFlow(loginFlow v1alpha1.IDPFlow, flowSource string) oidcclient.Option
|
||||
WithUpstreamIdentityProvider(upstreamName, upstreamType string) oidcclient.Option
|
||||
}
|
||||
|
||||
// clientOptions implements OIDCClientOptions for production use.
|
||||
type clientOptions struct{}
|
||||
|
||||
var _ OIDCClientOptions = (*clientOptions)(nil)
|
||||
|
||||
func (o *clientOptions) WithContext(ctx context.Context) oidcclient.Option {
|
||||
return oidcclient.WithContext(ctx)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithLogger(logger logr.Logger) oidcclient.Option {
|
||||
return oidcclient.WithLogger(logger)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithListenPort(port uint16) oidcclient.Option {
|
||||
return oidcclient.WithListenPort(port)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSkipBrowserOpen() oidcclient.Option {
|
||||
return oidcclient.WithSkipBrowserOpen()
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSkipListen() oidcclient.Option {
|
||||
return oidcclient.WithSkipListen()
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSkipPrintLoginURL() oidcclient.Option {
|
||||
return oidcclient.WithSkipPrintLoginURL()
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithSessionCache(cache oidcclient.SessionCache) oidcclient.Option {
|
||||
return oidcclient.WithSessionCache(cache)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithClient(httpClient *http.Client) oidcclient.Option {
|
||||
return oidcclient.WithClient(httpClient)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithScopes(scopes []string) oidcclient.Option {
|
||||
return oidcclient.WithScopes(scopes)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithRequestAudience(audience string) oidcclient.Option {
|
||||
return oidcclient.WithRequestAudience(audience)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithLoginFlow(loginFlow v1alpha1.IDPFlow, flowSource string) oidcclient.Option {
|
||||
return oidcclient.WithLoginFlow(loginFlow, flowSource)
|
||||
}
|
||||
|
||||
func (o *clientOptions) WithUpstreamIdentityProvider(upstreamName, upstreamType string) oidcclient.Option {
|
||||
return oidcclient.WithUpstreamIdentityProvider(upstreamName, upstreamType)
|
||||
}
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: go.pinniped.dev/internal/dynamiccert (interfaces: Private)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination=mockdynamiccert.go -package=mocks -copyright_file=../../../../hack/header.txt -mock_names Private=MockDynamicCertPrivate go.pinniped.dev/internal/dynamiccert Private
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
@@ -46,7 +51,7 @@ func (m *MockDynamicCertPrivate) AddListener(arg0 dynamiccertificates.Listener)
|
||||
}
|
||||
|
||||
// AddListener indicates an expected call of AddListener.
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) AddListener(arg0 interface{}) *gomock.Call {
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) AddListener(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddListener", reflect.TypeOf((*MockDynamicCertPrivate)(nil).AddListener), arg0)
|
||||
}
|
||||
@@ -87,7 +92,7 @@ func (m *MockDynamicCertPrivate) Run(arg0 context.Context, arg1 int) {
|
||||
}
|
||||
|
||||
// Run indicates an expected call of Run.
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) Run(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) Run(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockDynamicCertPrivate)(nil).Run), arg0, arg1)
|
||||
}
|
||||
@@ -101,7 +106,7 @@ func (m *MockDynamicCertPrivate) RunOnce(arg0 context.Context) error {
|
||||
}
|
||||
|
||||
// RunOnce indicates an expected call of RunOnce.
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) RunOnce(arg0 interface{}) *gomock.Call {
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) RunOnce(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunOnce", reflect.TypeOf((*MockDynamicCertPrivate)(nil).RunOnce), arg0)
|
||||
}
|
||||
@@ -115,7 +120,7 @@ func (m *MockDynamicCertPrivate) SetCertKeyContent(arg0, arg1 []byte) error {
|
||||
}
|
||||
|
||||
// SetCertKeyContent indicates an expected call of SetCertKeyContent.
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) SetCertKeyContent(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockDynamicCertPrivateMockRecorder) SetCertKeyContent(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCertKeyContent", reflect.TypeOf((*MockDynamicCertPrivate)(nil).SetCertKeyContent), arg0, arg1)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: go.pinniped.dev/internal/controller/kubecertagent (interfaces: PodCommandExecutor)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination=mockpodcommandexecutor.go -package=mocks -copyright_file=../../../../hack/header.txt go.pinniped.dev/internal/controller/kubecertagent PodCommandExecutor
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
@@ -41,7 +46,7 @@ func (m *MockPodCommandExecutor) EXPECT() *MockPodCommandExecutorMockRecorder {
|
||||
// Exec mocks base method.
|
||||
func (m *MockPodCommandExecutor) Exec(arg0 context.Context, arg1, arg2, arg3 string, arg4 ...string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0, arg1, arg2, arg3}
|
||||
varargs := []any{arg0, arg1, arg2, arg3}
|
||||
for _, a := range arg4 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
@@ -52,8 +57,8 @@ func (m *MockPodCommandExecutor) Exec(arg0 context.Context, arg1, arg2, arg3 str
|
||||
}
|
||||
|
||||
// Exec indicates an expected call of Exec.
|
||||
func (mr *MockPodCommandExecutorMockRecorder) Exec(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
|
||||
func (mr *MockPodCommandExecutorMockRecorder) Exec(arg0, arg1, arg2, arg3 any, arg4 ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
|
||||
varargs := append([]any{arg0, arg1, arg2, arg3}, arg4...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockPodCommandExecutor)(nil).Exec), varargs...)
|
||||
}
|
||||
|
||||
6
internal/mocks/mockoidcclientoptions/generate.go
Normal file
6
internal/mocks/mockoidcclientoptions/generate.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mockoidcclientoptions
|
||||
|
||||
//go:generate go run -v go.uber.org/mock/mockgen -destination=mockoidcclientoptions.go -package=mockoidcclientoptions -copyright_file=../../../hack/header.txt go.pinniped.dev/cmd/pinniped/cmd OIDCClientOptions
|
||||
216
internal/mocks/mockoidcclientoptions/mockoidcclientoptions.go
Normal file
216
internal/mocks/mockoidcclientoptions/mockoidcclientoptions.go
Normal file
@@ -0,0 +1,216 @@
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: go.pinniped.dev/cmd/pinniped/cmd (interfaces: OIDCClientOptions)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination=mockoidcclientoptions.go -package=mockoidcclientoptions -copyright_file=../../../hack/header.txt go.pinniped.dev/cmd/pinniped/cmd OIDCClientOptions
|
||||
//
|
||||
|
||||
// Package mockoidcclientoptions is a generated GoMock package.
|
||||
package mockoidcclientoptions
|
||||
|
||||
import (
|
||||
context "context"
|
||||
http "net/http"
|
||||
reflect "reflect"
|
||||
|
||||
logr "github.com/go-logr/logr"
|
||||
v1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||
oidcclient "go.pinniped.dev/pkg/oidcclient"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockOIDCClientOptions is a mock of OIDCClientOptions interface.
|
||||
type MockOIDCClientOptions struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockOIDCClientOptionsMockRecorder
|
||||
}
|
||||
|
||||
// MockOIDCClientOptionsMockRecorder is the mock recorder for MockOIDCClientOptions.
|
||||
type MockOIDCClientOptionsMockRecorder struct {
|
||||
mock *MockOIDCClientOptions
|
||||
}
|
||||
|
||||
// NewMockOIDCClientOptions creates a new mock instance.
|
||||
func NewMockOIDCClientOptions(ctrl *gomock.Controller) *MockOIDCClientOptions {
|
||||
mock := &MockOIDCClientOptions{ctrl: ctrl}
|
||||
mock.recorder = &MockOIDCClientOptionsMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockOIDCClientOptions) EXPECT() *MockOIDCClientOptionsMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// WithClient mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithClient(arg0 *http.Client) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithClient", arg0)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithClient indicates an expected call of WithClient.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithClient(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithClient", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithClient), arg0)
|
||||
}
|
||||
|
||||
// WithContext mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithContext(arg0 context.Context) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithContext", arg0)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithContext indicates an expected call of WithContext.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithContext(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithContext", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithContext), arg0)
|
||||
}
|
||||
|
||||
// WithListenPort mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithListenPort(arg0 uint16) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithListenPort", arg0)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithListenPort indicates an expected call of WithListenPort.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithListenPort(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithListenPort", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithListenPort), arg0)
|
||||
}
|
||||
|
||||
// WithLogger mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithLogger(arg0 logr.Logger) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithLogger", arg0)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithLogger indicates an expected call of WithLogger.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithLogger(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithLogger", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithLogger), arg0)
|
||||
}
|
||||
|
||||
// WithLoginFlow mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithLoginFlow(arg0 v1alpha1.IDPFlow, arg1 string) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithLoginFlow", arg0, arg1)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithLoginFlow indicates an expected call of WithLoginFlow.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithLoginFlow(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithLoginFlow", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithLoginFlow), arg0, arg1)
|
||||
}
|
||||
|
||||
// WithRequestAudience mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithRequestAudience(arg0 string) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithRequestAudience", arg0)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithRequestAudience indicates an expected call of WithRequestAudience.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithRequestAudience(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithRequestAudience", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithRequestAudience), arg0)
|
||||
}
|
||||
|
||||
// WithScopes mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithScopes(arg0 []string) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithScopes", arg0)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithScopes indicates an expected call of WithScopes.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithScopes(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithScopes", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithScopes), arg0)
|
||||
}
|
||||
|
||||
// WithSessionCache mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithSessionCache(arg0 oidcclient.SessionCache) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithSessionCache", arg0)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithSessionCache indicates an expected call of WithSessionCache.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithSessionCache(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithSessionCache", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithSessionCache), arg0)
|
||||
}
|
||||
|
||||
// WithSkipBrowserOpen mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithSkipBrowserOpen() oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithSkipBrowserOpen")
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithSkipBrowserOpen indicates an expected call of WithSkipBrowserOpen.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithSkipBrowserOpen() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithSkipBrowserOpen", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithSkipBrowserOpen))
|
||||
}
|
||||
|
||||
// WithSkipListen mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithSkipListen() oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithSkipListen")
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithSkipListen indicates an expected call of WithSkipListen.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithSkipListen() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithSkipListen", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithSkipListen))
|
||||
}
|
||||
|
||||
// WithSkipPrintLoginURL mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithSkipPrintLoginURL() oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithSkipPrintLoginURL")
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithSkipPrintLoginURL indicates an expected call of WithSkipPrintLoginURL.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithSkipPrintLoginURL() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithSkipPrintLoginURL", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithSkipPrintLoginURL))
|
||||
}
|
||||
|
||||
// WithUpstreamIdentityProvider mocks base method.
|
||||
func (m *MockOIDCClientOptions) WithUpstreamIdentityProvider(arg0, arg1 string) oidcclient.Option {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "WithUpstreamIdentityProvider", arg0, arg1)
|
||||
ret0, _ := ret[0].(oidcclient.Option)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WithUpstreamIdentityProvider indicates an expected call of WithUpstreamIdentityProvider.
|
||||
func (mr *MockOIDCClientOptionsMockRecorder) WithUpstreamIdentityProvider(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithUpstreamIdentityProvider", reflect.TypeOf((*MockOIDCClientOptions)(nil).WithUpstreamIdentityProvider), arg0, arg1)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -27,8 +28,8 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/term"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/strings/slices"
|
||||
|
||||
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
||||
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||
"go.pinniped.dev/internal/federationdomain/upstreamprovider"
|
||||
"go.pinniped.dev/internal/httputil/httperr"
|
||||
@@ -88,8 +89,9 @@ type handlerState struct {
|
||||
|
||||
// Tracking the usage of some other functional options.
|
||||
upstreamIdentityProviderName string
|
||||
upstreamIdentityProviderType string
|
||||
upstreamIdentityProviderType idpdiscoveryv1alpha1.IDPType
|
||||
cliToSendCredentials bool
|
||||
loginFlow idpdiscoveryv1alpha1.IDPFlow
|
||||
skipBrowser bool
|
||||
skipPrintLoginURL bool
|
||||
requestedAudience string
|
||||
@@ -101,6 +103,7 @@ type handlerState struct {
|
||||
|
||||
// Generated parameters of a login flow.
|
||||
provider *coreosoidc.Provider
|
||||
idpDiscovery *idpdiscoveryv1alpha1.IDPDiscoveryResponse
|
||||
oauth2Config *oauth2.Config
|
||||
useFormPost bool
|
||||
state state.State
|
||||
@@ -243,6 +246,9 @@ func WithRequestAudience(audience string) Option {
|
||||
// and by OIDCIdentityProviders which optionally enable the resource owner password credentials grant flow.
|
||||
// This should never be used with non-Supervisor issuers because it will send the user's password to the authorization
|
||||
// endpoint as a custom header, which would be ignored but could potentially get logged somewhere by the issuer.
|
||||
//
|
||||
// Deprecated: this option will be removed in a future version of Pinniped. See the WithLoginFlow() option instead.
|
||||
// If this option is used along with the WithLoginFlow() option, it will cause an error.
|
||||
func WithCLISendingCredentials() Option {
|
||||
return func(h *handlerState) error {
|
||||
h.cliToSendCredentials = true
|
||||
@@ -250,6 +256,38 @@ func WithCLISendingCredentials() Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithLoginFlow chooses the login flow.
|
||||
// When the argument is equal to idpdiscoveryv1alpha1.IDPFlowCLIPassword, it causes the login flow to use CLI-based
|
||||
// prompts for username and password and causes the call to the Issuer's authorize endpoint to be made directly (no web
|
||||
// browser) with the username and password on custom HTTP headers. This is only intended to be used when the issuer is a
|
||||
// Pinniped Supervisor and the upstream identity provider type supports this style of authentication. Currently, this is
|
||||
// supported by LDAPIdentityProviders, ActiveDirectoryIdentityProviders, and by OIDCIdentityProviders which optionally
|
||||
// enable the resource owner password credentials grant flow. This should never be used with non-Supervisor issuers
|
||||
// because it will send the user's password to the authorization endpoint as a custom header, which would be ignored but
|
||||
// could potentially get logged somewhere by the issuer.
|
||||
// When the argument is equal to idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode, it will attempt to open a web browser
|
||||
// and perform the OIDC authcode flow.
|
||||
// When not used, the default when the issuer is a Pinniped Supervisor will be determined automatically,
|
||||
// and the default for non-Supervisor issuers will be the browser authcode flow.
|
||||
func WithLoginFlow(loginFlow idpdiscoveryv1alpha1.IDPFlow, flowSource string) Option {
|
||||
return func(h *handlerState) error {
|
||||
switch loginFlow {
|
||||
case idpdiscoveryv1alpha1.IDPFlowCLIPassword,
|
||||
idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode:
|
||||
default:
|
||||
return fmt.Errorf(
|
||||
"WithLoginFlow error: loginFlow '%s' from '%s' must be '%s' or '%s'",
|
||||
loginFlow,
|
||||
flowSource,
|
||||
idpdiscoveryv1alpha1.IDPFlowCLIPassword,
|
||||
idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode,
|
||||
)
|
||||
}
|
||||
h.loginFlow = loginFlow
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUpstreamIdentityProvider causes the specified name and type to be sent as custom query parameters to the
|
||||
// issuer's authorize endpoint. This is only intended to be used when the issuer is a Pinniped Supervisor, in which
|
||||
// case it provides a mechanism to choose among several upstream identity providers.
|
||||
@@ -257,7 +295,10 @@ func WithCLISendingCredentials() Option {
|
||||
func WithUpstreamIdentityProvider(upstreamName, upstreamType string) Option {
|
||||
return func(h *handlerState) error {
|
||||
h.upstreamIdentityProviderName = upstreamName
|
||||
h.upstreamIdentityProviderType = upstreamType
|
||||
|
||||
// Do not perform validation on this cast.
|
||||
// If possible, dynamic validation against a Pinniped Supervisor's supported IDP types will be performed.
|
||||
h.upstreamIdentityProviderType = idpdiscoveryv1alpha1.IDPType(upstreamType)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -304,6 +345,13 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
|
||||
}
|
||||
}
|
||||
|
||||
if h.cliToSendCredentials {
|
||||
if h.loginFlow != "" {
|
||||
return nil, fmt.Errorf("do not use deprecated option WithCLISendingCredentials when using option WithLoginFlow")
|
||||
}
|
||||
h.loginFlow = idpdiscoveryv1alpha1.IDPFlowCLIPassword
|
||||
}
|
||||
|
||||
// Copy the configured HTTP client to set a request timeout (the Go default client has no timeout configured).
|
||||
httpClientWithTimeout := *h.httpClient
|
||||
httpClientWithTimeout.Timeout = httpRequestTimeout
|
||||
@@ -426,19 +474,24 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) {
|
||||
h.pkce.Challenge(),
|
||||
h.pkce.Method(),
|
||||
}
|
||||
if h.upstreamIdentityProviderName != "" {
|
||||
authorizeOptions = append(authorizeOptions,
|
||||
oauth2.SetAuthURLParam(oidcapi.AuthorizeUpstreamIDPNameParamName, h.upstreamIdentityProviderName),
|
||||
)
|
||||
authorizeOptions = append(authorizeOptions,
|
||||
oauth2.SetAuthURLParam(oidcapi.AuthorizeUpstreamIDPTypeParamName, h.upstreamIdentityProviderType),
|
||||
)
|
||||
|
||||
loginFlow, pinnipedSupervisorOptions, err := h.maybePerformPinnipedSupervisorValidations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.loginFlow = loginFlow
|
||||
authorizeOptions = append(authorizeOptions, pinnipedSupervisorOptions...)
|
||||
|
||||
// Preserve the legacy behavior where browser-based auth is preferred
|
||||
authFunc := h.webBrowserBasedAuth
|
||||
|
||||
// Choose the appropriate authorization and authcode exchange strategy.
|
||||
var authFunc = h.webBrowserBasedAuth
|
||||
if h.cliToSendCredentials {
|
||||
// Use a switch so that lint will make sure we have full coverage.
|
||||
switch h.loginFlow {
|
||||
case idpdiscoveryv1alpha1.IDPFlowCLIPassword:
|
||||
authFunc = h.cliBasedAuth
|
||||
case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode:
|
||||
// NOOP
|
||||
}
|
||||
|
||||
// Perform the authorize request and authcode exchange to get back OIDC tokens.
|
||||
@@ -452,6 +505,108 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) {
|
||||
return token, err
|
||||
}
|
||||
|
||||
// maybePerformPinnipedSupervisorValidations will return the flow and some authorization options.
|
||||
// When the IDP name is unset, it will assume that the server is not a Pinniped Supervisor, and will return immediately.
|
||||
// Otherwise, when the flow is unset, it will infer the flow from the server, or when the flow is set, it will return that flow unchanged.
|
||||
// It will also perform additional validations if the issuer is a Pinniped Supervisor.
|
||||
func (h *handlerState) maybePerformPinnipedSupervisorValidations() (idpdiscoveryv1alpha1.IDPFlow, []oauth2.AuthCodeOption, error) {
|
||||
loginFlow := h.loginFlow
|
||||
|
||||
if h.upstreamIdentityProviderName == "" {
|
||||
return loginFlow, nil, nil
|
||||
}
|
||||
|
||||
if h.idpDiscovery == nil {
|
||||
return "", nil, fmt.Errorf("upstream identity provider name %q was specified, but OIDC issuer %q does not "+
|
||||
"offer Pinniped-style IDP discovery, so it does not appear to be a Pinniped Supervisor; "+
|
||||
"specifying an upstream identity provider name is only meant to be used with Pinniped Supervisors",
|
||||
h.upstreamIdentityProviderName, h.issuer)
|
||||
}
|
||||
|
||||
// Legacy Pinniped Supervisors do not provide this information. Only run this validation when the information was provided.
|
||||
if len(h.idpDiscovery.PinnipedSupportedIDPTypes) > 0 {
|
||||
var supportedIDPTypes []idpdiscoveryv1alpha1.IDPType
|
||||
for _, idpType := range h.idpDiscovery.PinnipedSupportedIDPTypes {
|
||||
supportedIDPTypes = append(supportedIDPTypes, idpType.Type)
|
||||
}
|
||||
|
||||
// Sort by name for repeatability
|
||||
slices.Sort(supportedIDPTypes)
|
||||
|
||||
if !slices.Contains(supportedIDPTypes, h.upstreamIdentityProviderType) {
|
||||
convertIDPListToQuotedStringList := func() []string {
|
||||
var temp []string
|
||||
for _, idpType := range supportedIDPTypes {
|
||||
temp = append(temp, fmt.Sprintf("%q", idpType))
|
||||
}
|
||||
return temp
|
||||
}
|
||||
return "", nil, fmt.Errorf("unable to find upstream identity provider with type %q, this Pinniped Supervisor supports IDP types [%s]",
|
||||
h.upstreamIdentityProviderType,
|
||||
strings.Join(convertIDPListToQuotedStringList(), ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// Find the IDP from discovery by the specified name, type, and maybe flow.
|
||||
foundIDPIndex := slices.IndexFunc(h.idpDiscovery.PinnipedIDPs, func(idp idpdiscoveryv1alpha1.PinnipedIDP) bool {
|
||||
return idp.Name == h.upstreamIdentityProviderName &&
|
||||
idp.Type == h.upstreamIdentityProviderType &&
|
||||
(loginFlow == "" || slices.Contains(idp.Flows, loginFlow))
|
||||
})
|
||||
|
||||
// If the IDP was not found...
|
||||
if foundIDPIndex < 0 {
|
||||
pinnipedIDPsString, err := json.Marshal(h.idpDiscovery.PinnipedIDPs)
|
||||
if err != nil {
|
||||
// This should never happen. Not unit tested.
|
||||
return "", nil, fmt.Errorf("error marshalling IDP discovery response: %w", err)
|
||||
}
|
||||
if loginFlow == "" {
|
||||
return "", nil, fmt.Errorf(
|
||||
"unable to find upstream identity provider with name %q and type %q. Found these providers: %s",
|
||||
h.upstreamIdentityProviderName,
|
||||
h.upstreamIdentityProviderType,
|
||||
pinnipedIDPsString,
|
||||
)
|
||||
}
|
||||
return "", nil, fmt.Errorf(
|
||||
"unable to find upstream identity provider with name %q and type %q and flow %q. Found these providers: %s",
|
||||
h.upstreamIdentityProviderName,
|
||||
h.upstreamIdentityProviderType,
|
||||
loginFlow,
|
||||
pinnipedIDPsString,
|
||||
)
|
||||
}
|
||||
|
||||
// If the caller has not requested a specific flow, but has requested a specific IDP, infer the authentication flow
|
||||
// from the found IDP's discovery information.
|
||||
if loginFlow == "" {
|
||||
foundIDP := h.idpDiscovery.PinnipedIDPs[foundIDPIndex]
|
||||
if len(foundIDP.Flows) == 0 {
|
||||
// Note that this should not really happen because the Supervisor's IDP discovery endpoint has always listed flows.
|
||||
return "", nil, fmt.Errorf("unable to infer flow for upstream identity provider with name %q and type %q "+
|
||||
"because there were no flows discovered for that provider",
|
||||
h.upstreamIdentityProviderName,
|
||||
h.upstreamIdentityProviderType,
|
||||
)
|
||||
}
|
||||
// The order of the flows returned by the server indicates the server's flow preference,
|
||||
// so always use the first flow for that IDP from the discovery response.
|
||||
loginFlow = foundIDP.Flows[0]
|
||||
}
|
||||
|
||||
var authorizeOptions []oauth2.AuthCodeOption
|
||||
|
||||
authorizeOptions = append(authorizeOptions,
|
||||
oauth2.SetAuthURLParam(oidcapi.AuthorizeUpstreamIDPNameParamName, h.upstreamIdentityProviderName),
|
||||
)
|
||||
authorizeOptions = append(authorizeOptions,
|
||||
oauth2.SetAuthURLParam(oidcapi.AuthorizeUpstreamIDPTypeParamName, string(h.upstreamIdentityProviderType)),
|
||||
)
|
||||
|
||||
return loginFlow, authorizeOptions, nil
|
||||
}
|
||||
|
||||
// Make a direct call to the authorize endpoint, including the user's username and password on custom http headers,
|
||||
// and parse the authcode from the response. Exchange the authcode for tokens. Return the tokens or an error.
|
||||
func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (*oidctypes.Token, error) {
|
||||
@@ -759,7 +914,7 @@ func promptForSecret(promptLabel string, out io.Writer) (string, error) {
|
||||
}
|
||||
|
||||
func (h *handlerState) initOIDCDiscovery() error {
|
||||
// Make this method idempotent so it can be called in multiple cases with no extra network requests.
|
||||
// Make this method idempotent, so it can be called in multiple cases with no extra network requests.
|
||||
if h.provider != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -803,6 +958,60 @@ func (h *handlerState) initOIDCDiscovery() error {
|
||||
return fmt.Errorf("could not decode response_modes_supported in OIDC discovery from %q: %w", h.issuer, err)
|
||||
}
|
||||
h.useFormPost = slices.Contains(discoveryClaims.ResponseModesSupported, "form_post")
|
||||
|
||||
return h.maybePerformPinnipedSupervisorIDPDiscovery()
|
||||
}
|
||||
|
||||
func (h *handlerState) maybePerformPinnipedSupervisorIDPDiscovery() error {
|
||||
// If this OIDC IDP is a Pinniped Supervisor, it will have a reference to the IDP discovery document.
|
||||
// Go to that document and retrieve the IDPs.
|
||||
var pinnipedSupervisorClaims idpdiscoveryv1alpha1.OIDCDiscoveryResponse
|
||||
if err := h.provider.Claims(&pinnipedSupervisorClaims); err != nil {
|
||||
return fmt.Errorf("could not decode the Pinniped IDP discovery document URL in OIDC discovery from %q: %w", h.issuer, err)
|
||||
}
|
||||
|
||||
// This is not an error - it just means that this issuer is not a Pinniped Supervisor.
|
||||
// Note that this package can be used with OIDC IDPs other than Pinniped Supervisor.
|
||||
if pinnipedSupervisorClaims.SupervisorDiscovery.PinnipedIDPsEndpoint == "" {
|
||||
return nil
|
||||
}
|
||||
// This check confirms that the issuer is hosting the IDP discovery document, which would always be the case for
|
||||
// Pinniped Supervisor. Since there are checks above to confirm that the issuer uses HTTPS, IDP discovery will
|
||||
// always use HTTPS.
|
||||
if !strings.HasPrefix(pinnipedSupervisorClaims.SupervisorDiscovery.PinnipedIDPsEndpoint, h.issuer) {
|
||||
return fmt.Errorf("the Pinniped IDP discovery document must always be hosted by the issuer: %q", h.issuer)
|
||||
}
|
||||
|
||||
idpDiscoveryCtx, idpDiscoveryCtxCancelFunc := context.WithTimeout(h.ctx, httpRequestTimeout)
|
||||
defer idpDiscoveryCtxCancelFunc()
|
||||
idpDiscoveryReq, err := http.NewRequestWithContext(idpDiscoveryCtx, http.MethodGet, pinnipedSupervisorClaims.SupervisorDiscovery.PinnipedIDPsEndpoint, nil)
|
||||
if err != nil { // untested
|
||||
return fmt.Errorf("could not build IDP Discovery request: %w", err)
|
||||
}
|
||||
idpDiscoveryRes, err := h.httpClient.Do(idpDiscoveryReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IDP Discovery response error: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = idpDiscoveryRes.Body.Close() // We can't do anything if this fails to close
|
||||
}()
|
||||
|
||||
if idpDiscoveryRes.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unable to fetch IDP discovery data from issuer: unexpected http response status: %s", idpDiscoveryRes.Status)
|
||||
}
|
||||
|
||||
rawBody, err := io.ReadAll(idpDiscoveryRes.Body)
|
||||
if err != nil { // untested
|
||||
return fmt.Errorf("unable to fetch IDP discovery data from issuer: could not read response body: %w", err)
|
||||
}
|
||||
|
||||
var body idpdiscoveryv1alpha1.IDPDiscoveryResponse
|
||||
err = json.Unmarshal(rawBody, &body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch the Pinniped IDP discovery document: could not parse response JSON: %w", err)
|
||||
}
|
||||
|
||||
h.idpDiscovery = &body
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -701,27 +701,15 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
ptyFile, err := pty.Start(kubectlCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the subprocess to print the username prompt, then type the user's username.
|
||||
readFromFileUntilStringIsSeen(t, ptyFile, "Username: ")
|
||||
_, err = ptyFile.WriteString(env.SupervisorUpstreamOIDC.Username + "\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the subprocess to print the password prompt, then type the user's password.
|
||||
readFromFileUntilStringIsSeen(t, ptyFile, "Password: ")
|
||||
_, err = ptyFile.WriteString(env.SupervisorUpstreamOIDC.Password + "\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read all output from the subprocess until EOF.
|
||||
// Ignore any errors returned because there is always an error on linux.
|
||||
kubectlOutputBytes, _ := io.ReadAll(ptyFile)
|
||||
kubectlOutput := string(kubectlOutputBytes)
|
||||
|
||||
// The output should look like an authentication failure, because the OIDCIdentityProvider disallows password grants.
|
||||
// The output should fail IDP discovery validation, because the OIDCIdentityProvider disallows password grants.
|
||||
t.Log("kubectl command output (expecting a login failed error):\n", kubectlOutput)
|
||||
require.Contains(t, kubectlOutput,
|
||||
`Error: could not complete Pinniped login: login failed with code "access_denied": `+
|
||||
`The resource owner or authorization server denied the request. `+
|
||||
`Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.`,
|
||||
fmt.Sprintf(`could not complete Pinniped login: unable to find upstream identity provider with name "%[1]s" and type "oidc" and flow "cli_password". Found these providers: [{"name":"%[1]s","type":"oidc","flows":["browser_authcode"]}]`, oidcIdentityProvider.Name),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user