Merge pull request #560 from vmware-tanzu/client-debug-logging

Client debug logging
This commit is contained in:
Margo Crawford 2021-05-04 13:46:47 -07:00 committed by GitHub
commit 1a2940c278
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 327 additions and 43 deletions

View File

@ -20,10 +20,12 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/transport"
"k8s.io/klog/v2/klogr" "k8s.io/klog/v2/klogr"
"go.pinniped.dev/internal/execcredcache" "go.pinniped.dev/internal/execcredcache"
"go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/conciergeclient"
"go.pinniped.dev/pkg/oidcclient" "go.pinniped.dev/pkg/oidcclient"
"go.pinniped.dev/pkg/oidcclient/filesession" "go.pinniped.dev/pkg/oidcclient/filesession"
@ -36,13 +38,15 @@ func init() {
} }
type oidcLoginCommandDeps struct { type oidcLoginCommandDeps struct {
lookupEnv func(string) (string, bool)
login func(string, string, ...oidcclient.Option) (*oidctypes.Token, error) login func(string, string, ...oidcclient.Option) (*oidctypes.Token, error)
exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error) exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error)
} }
func oidcLoginCommandRealDeps() oidcLoginCommandDeps { func oidcLoginCommandRealDeps() oidcLoginCommandDeps {
return oidcLoginCommandDeps{ return oidcLoginCommandDeps{
login: oidcclient.Login, lookupEnv: os.LookupEnv,
login: oidcclient.Login,
exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) { exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) {
return client.ExchangeToken(ctx, token) return client.ExchangeToken(ctx, token)
}, },
@ -110,6 +114,11 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
} }
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error {
pLogger, err := SetLogLevel(deps.lookupEnv)
if err != nil {
plog.WarningErr("Received error while setting log level", err)
}
// Initialize the session cache. // Initialize the session cache.
var sessionOptions []filesession.Option var sessionOptions []filesession.Option
@ -125,6 +134,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
// Initialize the login handler. // Initialize the login handler.
opts := []oidcclient.Option{ opts := []oidcclient.Option{
oidcclient.WithContext(cmd.Context()), oidcclient.WithContext(cmd.Context()),
oidcclient.WithLogger(klogr.New()),
oidcclient.WithScopes(flags.scopes), oidcclient.WithScopes(flags.scopes),
oidcclient.WithSessionCache(sessionCache), oidcclient.WithSessionCache(sessionCache),
} }
@ -166,7 +176,6 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
} }
opts = append(opts, oidcclient.WithClient(client)) opts = append(opts, oidcclient.WithClient(client))
} }
// Look up cached credentials based on a hash of all the CLI arguments and the cluster info. // Look up cached credentials based on a hash of all the CLI arguments and the cluster info.
cacheKey := struct { cacheKey := struct {
Args []string `json:"args"` Args []string `json:"args"`
@ -179,10 +188,12 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
if flags.credentialCachePath != "" { if flags.credentialCachePath != "" {
credCache = execcredcache.New(flags.credentialCachePath) credCache = execcredcache.New(flags.credentialCachePath)
if cred := credCache.Get(cacheKey); cred != nil { if cred := credCache.Get(cacheKey); cred != nil {
pLogger.Debug("using cached cluster credential.")
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred) return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
} }
} }
pLogger.Debug("Performing OIDC login", "issuer", flags.issuer, "client id", flags.clientID)
// Do the basic login to get an OIDC token. // Do the basic login to get an OIDC token.
token, err := deps.login(flags.issuer, flags.clientID, opts...) token, err := deps.login(flags.issuer, flags.clientID, opts...)
if err != nil { if err != nil {
@ -192,6 +203,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential. // If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
if concierge != nil { if concierge != nil {
pLogger.Debug("Exchanging token for cluster credential", "endpoint", flags.conciergeEndpoint, "authenticator type", flags.conciergeAuthenticatorType, "authenticator name", flags.conciergeAuthenticatorName)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
@ -199,10 +211,14 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
if err != nil { if err != nil {
return fmt.Errorf("could not complete Concierge credential exchange: %w", err) return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
} }
pLogger.Debug("Successfully exchanged token for cluster credential.")
} else {
pLogger.Debug("No concierge configured, skipping token credential exchange")
} }
// If there was a credential cache, save the resulting credential for future use. // If there was a credential cache, save the resulting credential for future use.
if credCache != nil { if credCache != nil {
pLogger.Debug("caching cluster credential for future use.")
credCache.Put(cacheKey, cred) credCache.Put(cacheKey, cred)
} }
return json.NewEncoder(cmd.OutOrStdout()).Encode(cred) return json.NewEncoder(cmd.OutOrStdout()).Encode(cred)
@ -224,7 +240,7 @@ func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, er
} }
pool.AppendCertsFromPEM(pem) pool.AppendCertsFromPEM(pem)
} }
return &http.Client{ client := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
@ -232,7 +248,10 @@ func makeClient(caBundlePaths []string, caBundleData []string) (*http.Client, er
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
}, },
}, },
}, nil }
client.Transport = transport.DebugWrappers(client.Transport)
return client, nil
} }
func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential { func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
@ -251,6 +270,18 @@ func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
return &cred return &cred
} }
func SetLogLevel(lookupEnv func(string) (string, bool)) (*plog.PLogger, error) {
debug, _ := lookupEnv("PINNIPED_DEBUG")
if debug == "true" {
err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug)
if err != nil {
return nil, err
}
}
logger := plog.New("Pinniped login: ")
return &logger, nil
}
// mustGetConfigDir returns a directory that follows the XDG base directory convention: // mustGetConfigDir returns a directory that follows the XDG base directory convention:
// $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should // $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should
// be stored. If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used. // be stored. If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used.

View File

@ -16,10 +16,12 @@ 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"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/conciergeclient"
"go.pinniped.dev/pkg/oidcclient" "go.pinniped.dev/pkg/oidcclient"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
@ -41,10 +43,12 @@ func TestLoginOIDCCommand(t *testing.T) {
args []string args []string
loginErr error loginErr error
conciergeErr error conciergeErr error
env map[string]string
wantError bool wantError bool
wantStdout string wantStdout string
wantStderr string wantStderr string
wantOptionsCount int wantOptionsCount int
wantLogs []string
}{ }{
{ {
name: "help flag passed", name: "help flag passed",
@ -141,7 +145,7 @@ func TestLoginOIDCCommand(t *testing.T) {
"--issuer", "test-issuer", "--issuer", "test-issuer",
}, },
loginErr: fmt.Errorf("some login error"), loginErr: fmt.Errorf("some login error"),
wantOptionsCount: 3, wantOptionsCount: 4,
wantError: true, wantError: true,
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: could not complete Pinniped login: some login error Error: could not complete Pinniped login: some login error
@ -158,7 +162,7 @@ func TestLoginOIDCCommand(t *testing.T) {
"--concierge-endpoint", "https://127.0.0.1:1234/", "--concierge-endpoint", "https://127.0.0.1:1234/",
}, },
conciergeErr: fmt.Errorf("some concierge error"), conciergeErr: fmt.Errorf("some concierge error"),
wantOptionsCount: 3, wantOptionsCount: 4,
wantError: true, wantError: true,
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: could not complete Concierge credential exchange: some concierge error Error: could not complete Concierge credential exchange: some concierge error
@ -170,8 +174,14 @@ func TestLoginOIDCCommand(t *testing.T) {
"--client-id", "test-client-id", "--client-id", "test-client-id",
"--issuer", "test-issuer", "--issuer", "test-issuer",
}, },
wantOptionsCount: 3, env: map[string]string{"PINNIPED_DEBUG": "true"},
wantOptionsCount: 4,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
wantLogs: []string{
"\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"",
"\"level\"=0 \"msg\"=\"Pinniped login: No concierge configured, skipping token credential exchange\"",
"\"level\"=0 \"msg\"=\"Pinniped login: caching cluster credential for future use.\"",
},
}, },
{ {
name: "success with all options", name: "success with all options",
@ -192,17 +202,30 @@ func TestLoginOIDCCommand(t *testing.T) {
"--concierge-api-group-suffix", "some.suffix.com", "--concierge-api-group-suffix", "some.suffix.com",
"--credential-cache", testutil.TempDir(t) + "/credentials.yaml", "--credential-cache", testutil.TempDir(t) + "/credentials.yaml",
}, },
wantOptionsCount: 7, env: map[string]string{"PINNIPED_DEBUG": "true"},
wantOptionsCount: 8,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n",
wantLogs: []string{
"\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"",
"\"level\"=0 \"msg\"=\"Pinniped login: Exchanging token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1:1234/\"",
"\"level\"=0 \"msg\"=\"Pinniped login: Successfully exchanged token for cluster credential.\"",
"\"level\"=0 \"msg\"=\"Pinniped login: caching cluster credential for future use.\"",
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
testLogger := testlogger.New(t)
klog.SetLogger(testLogger)
var ( var (
gotOptions []oidcclient.Option gotOptions []oidcclient.Option
) )
cmd := oidcLoginCommand(oidcLoginCommandDeps{ cmd := oidcLoginCommand(oidcLoginCommandDeps{
lookupEnv: func(s string) (string, bool) {
v, ok := tt.env[s]
return v, ok
},
login: func(issuer string, clientID string, opts ...oidcclient.Option) (*oidctypes.Token, error) { login: func(issuer string, clientID string, opts ...oidcclient.Option) (*oidctypes.Token, error) {
require.Equal(t, "test-issuer", issuer) require.Equal(t, "test-issuer", issuer)
require.Equal(t, "test-client-id", clientID) require.Equal(t, "test-client-id", clientID)
@ -248,6 +271,8 @@ func TestLoginOIDCCommand(t *testing.T) {
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
require.Len(t, gotOptions, tt.wantOptionsCount) require.Len(t, gotOptions, tt.wantOptionsCount)
require.Equal(t, tt.wantLogs, testLogger.Lines())
}) })
} }
} }

View File

@ -17,6 +17,7 @@ import (
"go.pinniped.dev/internal/execcredcache" "go.pinniped.dev/internal/execcredcache"
"go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/conciergeclient"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
) )
@ -83,6 +84,11 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
} }
func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error { func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error {
pLogger, err := SetLogLevel(deps.lookupEnv)
if err != nil {
plog.WarningErr("Received error while setting log level", err)
}
if flags.staticToken == "" && flags.staticTokenEnvName == "" { if flags.staticToken == "" && flags.staticTokenEnvName == "" {
return fmt.Errorf("one of --token or --token-env must be set") return fmt.Errorf("one of --token or --token-env must be set")
} }
@ -131,12 +137,14 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
if flags.credentialCachePath != "" { if flags.credentialCachePath != "" {
credCache = execcredcache.New(flags.credentialCachePath) credCache = execcredcache.New(flags.credentialCachePath)
if cred := credCache.Get(cacheKey); cred != nil { if cred := credCache.Get(cacheKey); cred != nil {
pLogger.Debug("using cached cluster credential.")
return json.NewEncoder(out).Encode(cred) return json.NewEncoder(out).Encode(cred)
} }
} }
// If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential. // If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential.
if concierge != nil { if concierge != nil {
pLogger.Debug("exchanging static token for cluster credential", "endpoint", flags.conciergeEndpoint, "authenticator type", flags.conciergeAuthenticatorType, "authenticator name", flags.conciergeAuthenticatorName)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
@ -145,6 +153,7 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams
if err != nil { if err != nil {
return fmt.Errorf("could not complete Concierge credential exchange: %w", err) return fmt.Errorf("could not complete Concierge credential exchange: %w", err)
} }
pLogger.Debug("exchanged static token for cluster credential")
} }
// If there was a credential cache, save the resulting credential for future use. We only save to the cache if // If there was a credential cache, save the resulting credential for future use. We only save to the cache if

View File

@ -12,6 +12,10 @@ import (
"testing" "testing"
"time" "time"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/testutil/testlogger"
"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"
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
@ -41,6 +45,7 @@ func TestLoginStaticCommand(t *testing.T) {
wantStdout string wantStdout string
wantStderr string wantStderr string
wantOptionsCount int wantOptionsCount int
wantLogs []string
}{ }{
{ {
name: "help flag passed", name: "help flag passed",
@ -126,10 +131,12 @@ func TestLoginStaticCommand(t *testing.T) {
"--concierge-authenticator-name", "test-authenticator", "--concierge-authenticator-name", "test-authenticator",
}, },
conciergeErr: fmt.Errorf("some concierge error"), conciergeErr: fmt.Errorf("some concierge error"),
env: map[string]string{"PINNIPED_DEBUG": "true"},
wantError: true, wantError: true,
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: could not complete Concierge credential exchange: some concierge error Error: could not complete Concierge credential exchange: some concierge error
`), `),
wantLogs: []string{"\"level\"=0 \"msg\"=\"Pinniped login: exchanging static token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1/\""},
}, },
{ {
name: "invalid API group suffix", name: "invalid API group suffix",
@ -151,12 +158,15 @@ func TestLoginStaticCommand(t *testing.T) {
args: []string{ args: []string{
"--token", "test-token", "--token", "test-token",
}, },
env: map[string]string{"PINNIPED_DEBUG": "true"},
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"test-token"}}` + "\n", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"test-token"}}` + "\n",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
testLogger := testlogger.New(t)
klog.SetLogger(testLogger)
cmd := staticLoginCommand(staticLoginDeps{ cmd := staticLoginCommand(staticLoginDeps{
lookupEnv: func(s string) (string, bool) { lookupEnv: func(s string) (string, bool) {
v, ok := tt.env[s] v, ok := tt.env[s]
@ -192,6 +202,8 @@ func TestLoginStaticCommand(t *testing.T) {
} }
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
require.Equal(t, tt.wantLogs, testLogger.Lines())
}) })
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Package plog implements a thin layer over klog to help enforce pinniped's logging convention. // Package plog implements a thin layer over klog to help enforce pinniped's logging convention.
@ -26,56 +26,157 @@
// act of desperation to determine why the system is broken. // act of desperation to determine why the system is broken.
package plog package plog
import "k8s.io/klog/v2" import (
"k8s.io/klog/v2"
)
const errorKey = "error" const errorKey = "error"
// Use Error to log an unexpected system error. type _ interface {
func Error(msg string, err error, keysAndValues ...interface{}) { Error(msg string, err error, keysAndValues ...interface{})
klog.ErrorS(err, msg, keysAndValues...) Warning(msg string, keysAndValues ...interface{})
WarningErr(msg string, err error, keysAndValues ...interface{})
Info(msg string, keysAndValues ...interface{})
InfoErr(msg string, err error, keysAndValues ...interface{})
Debug(msg string, keysAndValues ...interface{})
DebugErr(msg string, err error, keysAndValues ...interface{})
Trace(msg string, keysAndValues ...interface{})
TraceErr(msg string, err error, keysAndValues ...interface{})
All(msg string, keysAndValues ...interface{})
} }
func Warning(msg string, keysAndValues ...interface{}) { type PLogger struct {
prefix string
depth int
}
func New(prefix string) PLogger {
return PLogger{
depth: 0,
prefix: prefix,
}
}
func (p *PLogger) Error(msg string, err error, keysAndValues ...interface{}) {
klog.ErrorSDepth(p.depth+1, err, p.prefix+msg, keysAndValues...)
}
func (p *PLogger) warningDepth(msg string, depth int, keysAndValues ...interface{}) {
// klog's structured logging has no concept of a warning (i.e. no WarningS function) // klog's structured logging has no concept of a warning (i.e. no WarningS function)
// Thus we use info at log level zero as a proxy // Thus we use info at log level zero as a proxy
// klog's info logs have an I prefix and its warning logs have a W prefix // klog's info logs have an I prefix and its warning logs have a W prefix
// Since we lose the W prefix by using InfoS, just add a key to make these easier to find // Since we lose the W prefix by using InfoS, just add a key to make these easier to find
keysAndValues = append([]interface{}{"warning", "true"}, keysAndValues...) keysAndValues = append([]interface{}{"warning", "true"}, keysAndValues...)
klog.V(klogLevelWarning).InfoS(msg, keysAndValues...) if klog.V(klogLevelWarning).Enabled() {
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
}
}
func (p *PLogger) Warning(msg string, keysAndValues ...interface{}) {
p.warningDepth(msg, p.depth+1, keysAndValues...)
}
// Use WarningErr to issue a Warning message with an error object as part of the message.
func (p *PLogger) WarningErr(msg string, err error, keysAndValues ...interface{}) {
p.warningDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func (p *PLogger) infoDepth(msg string, depth int, keysAndValues ...interface{}) {
if klog.V(klogLevelInfo).Enabled() {
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
}
}
func (p *PLogger) Info(msg string, keysAndValues ...interface{}) {
p.infoDepth(msg, p.depth+1, keysAndValues...)
}
// Use InfoErr to log an expected error, e.g. validation failure of an http parameter.
func (p *PLogger) InfoErr(msg string, err error, keysAndValues ...interface{}) {
p.infoDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func (p *PLogger) debugDepth(msg string, depth int, keysAndValues ...interface{}) {
if klog.V(klogLevelDebug).Enabled() {
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
}
}
func (p *PLogger) Debug(msg string, keysAndValues ...interface{}) {
p.debugDepth(msg, p.depth+1, keysAndValues...)
}
// Use DebugErr to issue a Debug message with an error object as part of the message.
func (p *PLogger) DebugErr(msg string, err error, keysAndValues ...interface{}) {
p.debugDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func (p *PLogger) traceDepth(msg string, depth int, keysAndValues ...interface{}) {
if klog.V(klogLevelTrace).Enabled() {
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
}
}
func (p *PLogger) Trace(msg string, keysAndValues ...interface{}) {
p.traceDepth(msg, p.depth+1, keysAndValues...)
}
// Use TraceErr to issue a Trace message with an error object as part of the message.
func (p *PLogger) TraceErr(msg string, err error, keysAndValues ...interface{}) {
p.traceDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
}
func (p *PLogger) All(msg string, keysAndValues ...interface{}) {
if klog.V(klogLevelAll).Enabled() {
klog.InfoSDepth(p.depth+1, p.prefix+msg, keysAndValues...)
}
}
var pLogger = PLogger{ //nolint:gochecknoglobals
depth: 1,
}
// Use Error to log an unexpected system error.
func Error(msg string, err error, keysAndValues ...interface{}) {
pLogger.Error(msg, err, keysAndValues...)
}
func Warning(msg string, keysAndValues ...interface{}) {
pLogger.Warning(msg, keysAndValues...)
} }
// Use WarningErr to issue a Warning message with an error object as part of the message. // Use WarningErr to issue a Warning message with an error object as part of the message.
func WarningErr(msg string, err error, keysAndValues ...interface{}) { func WarningErr(msg string, err error, keysAndValues ...interface{}) {
Warning(msg, append([]interface{}{errorKey, err}, keysAndValues...)...) pLogger.WarningErr(msg, err, keysAndValues...)
} }
func Info(msg string, keysAndValues ...interface{}) { func Info(msg string, keysAndValues ...interface{}) {
klog.V(klogLevelInfo).InfoS(msg, keysAndValues...) pLogger.Info(msg, keysAndValues...)
} }
// Use InfoErr to log an expected error, e.g. validation failure of an http parameter. // Use InfoErr to log an expected error, e.g. validation failure of an http parameter.
func InfoErr(msg string, err error, keysAndValues ...interface{}) { func InfoErr(msg string, err error, keysAndValues ...interface{}) {
Info(msg, append([]interface{}{errorKey, err}, keysAndValues...)...) pLogger.InfoErr(msg, err, keysAndValues...)
} }
func Debug(msg string, keysAndValues ...interface{}) { func Debug(msg string, keysAndValues ...interface{}) {
klog.V(klogLevelDebug).InfoS(msg, keysAndValues...) pLogger.Debug(msg, keysAndValues...)
} }
// Use DebugErr to issue a Debug message with an error object as part of the message. // Use DebugErr to issue a Debug message with an error object as part of the message.
func DebugErr(msg string, err error, keysAndValues ...interface{}) { func DebugErr(msg string, err error, keysAndValues ...interface{}) {
Debug(msg, append([]interface{}{errorKey, err}, keysAndValues...)...) pLogger.DebugErr(msg, err, keysAndValues...)
} }
func Trace(msg string, keysAndValues ...interface{}) { func Trace(msg string, keysAndValues ...interface{}) {
klog.V(klogLevelTrace).InfoS(msg, keysAndValues...) pLogger.Trace(msg, keysAndValues...)
} }
// Use TraceErr to issue a Trace message with an error object as part of the message. // Use TraceErr to issue a Trace message with an error object as part of the message.
func TraceErr(msg string, err error, keysAndValues ...interface{}) { func TraceErr(msg string, err error, keysAndValues ...interface{}) {
Trace(msg, append([]interface{}{errorKey, err}, keysAndValues...)...) pLogger.TraceErr(msg, err, keysAndValues...)
} }
func All(msg string, keysAndValues ...interface{}) { func All(msg string, keysAndValues ...interface{}) {
klog.V(klogLevelAll).InfoS(msg, keysAndValues...) pLogger.All(msg, keysAndValues...)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Package oidcclient implements a CLI OIDC login flow. // Package oidcclient implements a CLI OIDC login flow.
@ -17,6 +17,7 @@ import (
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/go-logr/logr"
"github.com/pkg/browser" "github.com/pkg/browser"
"golang.org/x/oauth2" "golang.org/x/oauth2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -44,11 +45,14 @@ const (
// overallTimeout is the overall time that a login is allowed to take. This includes several user interactions, so // overallTimeout is the overall time that a login is allowed to take. This includes several user interactions, so
// we set this to be relatively long. // we set this to be relatively long.
overallTimeout = 90 * time.Minute overallTimeout = 90 * time.Minute
debugLogLevel = 4
) )
type handlerState struct { type handlerState struct {
// Basic parameters. // Basic parameters.
ctx context.Context ctx context.Context
logger logr.Logger
issuer string issuer string
clientID string clientID string
scopes []string scopes []string
@ -97,6 +101,15 @@ func WithContext(ctx context.Context) Option {
} }
} }
// WithLogger specifies a PLogger to use with the login.
// If not specified this will default to a new logger.
func WithLogger(logger logr.Logger) Option {
return func(h *handlerState) error {
h.logger = logger
return nil
}
}
// WithListenPort specifies a TCP listen port on localhost, which will be used for the redirect_uri and to handle the // WithListenPort specifies a TCP listen port on localhost, which will be used for the redirect_uri and to handle the
// authorization code callback. By default, a random high port will be chosen which requires the authorization server // authorization code callback. By default, a random high port will be chosen which requires the authorization server
// to support wildcard port numbers as described by https://tools.ietf.org/html/rfc8252: // to support wildcard port numbers as described by https://tools.ietf.org/html/rfc8252:
@ -183,6 +196,7 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er
cache: &nopCache{}, cache: &nopCache{},
callbackPath: "/callback", callbackPath: "/callback",
ctx: context.Background(), ctx: context.Background(),
logger: logr.Discard(), // discard logs unless a logger is specified
callbacks: make(chan callbackResult), callbacks: make(chan callbackResult),
httpClient: http.DefaultClient, httpClient: http.DefaultClient,
@ -260,6 +274,7 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) {
// If the ID token is still valid for a bit, return it immediately and skip the rest of the flow. // If the ID token is still valid for a bit, return it immediately and skip the rest of the flow.
cached := h.cache.GetToken(cacheKey) cached := h.cache.GetToken(cacheKey)
if cached != nil && cached.IDToken != nil && time.Until(cached.IDToken.Expiry.Time) > minIDTokenValidity { if cached != nil && cached.IDToken != nil && time.Until(cached.IDToken.Expiry.Time) > minIDTokenValidity {
h.logger.V(debugLogLevel).Info("Pinniped: Found unexpired cached token.")
return cached, nil return cached, nil
} }
@ -327,6 +342,7 @@ func (h *handlerState) initOIDCDiscovery() error {
return nil return nil
} }
h.logger.V(debugLogLevel).Info("Pinniped: Performing OIDC discovery", "issuer", h.issuer)
var err error var err error
h.provider, err = oidc.NewProvider(h.ctx, h.issuer) h.provider, err = oidc.NewProvider(h.ctx, h.issuer)
if err != nil { if err != nil {
@ -343,6 +359,7 @@ func (h *handlerState) initOIDCDiscovery() error {
} }
func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidctypes.Token, error) { func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidctypes.Token, error) {
h.logger.V(debugLogLevel).Info("Pinniped: Performing RFC8693 token exchange", "requestedAudience", h.requestedAudience)
// Perform OIDC discovery. This may have already been performed if there was not a cached base token. // Perform OIDC discovery. This may have already been performed if there was not a cached base token.
if err := h.initOIDCDiscovery(); err != nil { if err := h.initOIDCDiscovery(); err != nil {
return nil, err return nil, err
@ -413,6 +430,7 @@ func (h *handlerState) tokenExchangeRFC8693(baseToken *oidctypes.Token) (*oidcty
} }
func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) { func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctypes.RefreshToken) (*oidctypes.Token, error) {
h.logger.V(debugLogLevel).Info("Pinniped: Refreshing cached token.")
refreshSource := h.oauth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken.Token}) refreshSource := h.oauth2Config.TokenSource(ctx, &oauth2.Token{RefreshToken: refreshToken.Token})
refreshed, err := refreshSource.Token() refreshed, err := refreshSource.Token()

View File

@ -13,17 +13,21 @@ import (
"testing" "testing"
"time" "time"
"github.com/go-logr/stdr"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider" "go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
"go.pinniped.dev/pkg/oidcclient/pkce" "go.pinniped.dev/pkg/oidcclient/pkce"
@ -205,6 +209,7 @@ func TestLogin(t *testing.T) {
clientID string clientID string
wantErr string wantErr string
wantToken *oidctypes.Token wantToken *oidctypes.Token
wantLogs []string
}{ }{
{ {
name: "option error", name: "option error",
@ -269,7 +274,8 @@ func TestLogin(t *testing.T) {
return WithSessionCache(cache)(h) return WithSessionCache(cache)(h)
} }
}, },
wantErr: `could not perform OIDC discovery for "test-issuer": Get "test-issuer/.well-known/openid-configuration": unsupported protocol scheme ""`, wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"test-issuer\""},
wantErr: `could not perform OIDC discovery for "test-issuer": Get "test-issuer/.well-known/openid-configuration": unsupported protocol scheme ""`,
}, },
{ {
name: "session cache hit with valid token", name: "session cache hit with valid token",
@ -290,6 +296,7 @@ func TestLogin(t *testing.T) {
return WithSessionCache(cache)(h) return WithSessionCache(cache)(h)
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\""},
wantToken: &testToken, wantToken: &testToken,
}, },
{ {
@ -297,8 +304,9 @@ func TestLogin(t *testing.T) {
opt: func(t *testing.T) Option { opt: func(t *testing.T) Option {
return func(h *handlerState) error { return nil } return func(h *handlerState) error { return nil }
}, },
issuer: errorServer.URL, issuer: errorServer.URL,
wantErr: fmt.Sprintf("could not perform OIDC discovery for %q: 500 Internal Server Error: some discovery error\n", errorServer.URL), wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + errorServer.URL + "\""},
wantErr: fmt.Sprintf("could not perform OIDC discovery for %q: 500 Internal Server Error: some discovery error\n", errorServer.URL),
}, },
{ {
name: "session cache hit with refreshable token", name: "session cache hit with refreshable token",
@ -337,6 +345,8 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\"",
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\""},
wantToken: &testToken, wantToken: &testToken,
}, },
{ {
@ -369,6 +379,8 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\"",
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\""},
wantErr: "some validation error", wantErr: "some validation error",
}, },
{ {
@ -395,6 +407,8 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\"",
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\""},
// Expect this to fall through to the authorization code flow, so it fails here. // Expect this to fall through to the authorization code flow, so it fails here.
wantErr: "could not open callback listener: listen tcp: address invalid-listen-address: missing port in address", wantErr: "could not open callback listener: listen tcp: address invalid-listen-address: missing port in address",
}, },
@ -406,8 +420,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
issuer: successServer.URL, issuer: successServer.URL,
wantErr: "could not open callback listener: listen tcp: address invalid-listen-address: missing port in address", wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: "could not open callback listener: listen tcp: address invalid-listen-address: missing port in address",
}, },
{ {
name: "browser open failure", name: "browser open failure",
@ -416,8 +431,9 @@ func TestLogin(t *testing.T) {
return fmt.Errorf("some browser open error") return fmt.Errorf("some browser open error")
}) })
}, },
issuer: successServer.URL, issuer: successServer.URL,
wantErr: "could not open browser: some browser open error", wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: "could not open browser: some browser open error",
}, },
{ {
name: "timeout waiting for callback", name: "timeout waiting for callback",
@ -433,8 +449,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
issuer: successServer.URL, issuer: successServer.URL,
wantErr: "timed out waiting for token callback: context canceled", wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: "timed out waiting for token callback: context canceled",
}, },
{ {
name: "callback returns error", name: "callback returns error",
@ -449,8 +466,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
issuer: successServer.URL, issuer: successServer.URL,
wantErr: "error handling callback: some callback error", wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: "error handling callback: some callback error",
}, },
{ {
name: "callback returns success", name: "callback returns success",
@ -510,6 +528,7 @@ func TestLogin(t *testing.T) {
} }
}, },
issuer: successServer.URL, issuer: successServer.URL,
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantToken: &testToken, wantToken: &testToken,
}, },
{ {
@ -533,6 +552,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"cluster-1234\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + errorServer.URL + "\""},
wantErr: fmt.Sprintf("failed to exchange token: could not perform OIDC discovery for %q: 500 Internal Server Error: some discovery error\n", errorServer.URL), wantErr: fmt.Sprintf("failed to exchange token: could not perform OIDC discovery for %q: 500 Internal Server Error: some discovery error\n", errorServer.URL),
}, },
{ {
@ -556,6 +578,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"cluster-1234\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + brokenTokenURLServer.URL + "\""},
wantErr: `failed to exchange token: could not build RFC8693 request: parse "%": invalid URL escape "%"`, wantErr: `failed to exchange token: could not build RFC8693 request: parse "%": invalid URL escape "%"`,
}, },
{ {
@ -579,6 +604,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-http-response\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: fmt.Sprintf(`failed to exchange token: Post "%s/token": failed to parse Location header "%%": parse "%%": invalid URL escape "%%"`, successServer.URL), wantErr: fmt.Sprintf(`failed to exchange token: Post "%s/token": failed to parse Location header "%%": parse "%%": invalid URL escape "%%"`, successServer.URL),
}, },
{ {
@ -602,6 +630,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-http-400\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: `failed to exchange token: unexpected HTTP response status 400`, wantErr: `failed to exchange token: unexpected HTTP response status 400`,
}, },
{ {
@ -625,6 +656,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-content-type\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: `failed to exchange token: failed to decode content-type header: mime: invalid media parameter`, wantErr: `failed to exchange token: failed to decode content-type header: mime: invalid media parameter`,
}, },
{ {
@ -648,6 +682,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-wrong-content-type\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: `failed to exchange token: unexpected HTTP response content type "invalid"`, wantErr: `failed to exchange token: unexpected HTTP response content type "invalid"`,
}, },
{ {
@ -671,6 +708,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-json\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: `failed to exchange token: failed to decode response: unexpected EOF`, wantErr: `failed to exchange token: failed to decode response: unexpected EOF`,
}, },
{ {
@ -694,6 +734,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-tokentype\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: `failed to exchange token: got unexpected token_type "invalid"`, wantErr: `failed to exchange token: got unexpected token_type "invalid"`,
}, },
{ {
@ -717,6 +760,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-issuedtokentype\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: `failed to exchange token: got unexpected issued_token_type "invalid"`, wantErr: `failed to exchange token: got unexpected issued_token_type "invalid"`,
}, },
{ {
@ -740,6 +786,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience-produce-invalid-jwt\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantErr: `failed to exchange token: received invalid JWT: oidc: malformed jwt: square/go-jose: compact JWS format must have three parts`, wantErr: `failed to exchange token: received invalid JWT: oidc: malformed jwt: square/go-jose: compact JWS format must have three parts`,
}, },
{ {
@ -769,6 +818,9 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{"\"level\"=4 \"msg\"=\"Pinniped: Found unexpired cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\""},
wantToken: &testExchangedToken, wantToken: &testExchangedToken,
}, },
{ {
@ -816,18 +868,29 @@ func TestLogin(t *testing.T) {
return nil return nil
} }
}, },
wantLogs: []string{
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\"",
"\"level\"=4 \"msg\"=\"Pinniped: Refreshing cached token.\"",
"\"level\"=4 \"msg\"=\"Pinniped: Performing RFC8693 token exchange\" \"requestedAudience\"=\"test-audience\"",
},
wantToken: &testExchangedToken, wantToken: &testExchangedToken,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
tt := tt tt := tt
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
testLogger := testlogger.New(t)
klog.SetLogger(testLogger)
stdr.SetVerbosity(debugLogLevel) // set stdr's global log level to debug so the test logger will send output.
tok, err := Login(tt.issuer, tt.clientID, tok, err := Login(tt.issuer, tt.clientID,
WithContext(context.Background()), WithContext(context.Background()),
WithListenPort(0), WithListenPort(0),
WithScopes([]string{"test-scope"}), WithScopes([]string{"test-scope"}),
tt.opt(t), tt.opt(t),
WithLogger(testLogger),
) )
require.Equal(t, tt.wantLogs, testLogger.Lines())
if tt.wantErr != "" { if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr) require.EqualError(t, err, tt.wantErr)
require.Nil(t, tok) require.Nil(t, tok)

View File

@ -208,6 +208,8 @@ func TestCLILoginOIDC(t *testing.T) {
require.NoErrorf(t, json.Unmarshal(cmd2Output, &credOutput2), require.NoErrorf(t, json.Unmarshal(cmd2Output, &credOutput2),
"command returned something other than an ExecCredential:\n%s", string(cmd2Output)) "command returned something other than an ExecCredential:\n%s", string(cmd2Output))
require.Equal(t, credOutput, credOutput2) require.Equal(t, credOutput, credOutput2)
// the logs contain only the ExecCredential. There are 2 elements because the last one is "".
require.Len(t, strings.Split(string(cmd2Output), "\n"), 2)
// Overwrite the cache entry to remove the access and ID tokens. // Overwrite the cache entry to remove the access and ID tokens.
t.Logf("overwriting cache to remove valid ID token") t.Logf("overwriting cache to remove valid ID token")
@ -237,6 +239,26 @@ func TestCLILoginOIDC(t *testing.T) {
require.NoErrorf(t, json.Unmarshal(cmd3Output, &credOutput3), require.NoErrorf(t, json.Unmarshal(cmd3Output, &credOutput3),
"command returned something other than an ExecCredential:\n%s", string(cmd2Output)) "command returned something other than an ExecCredential:\n%s", string(cmd2Output))
require.NotEqual(t, credOutput2.Status.Token, credOutput3.Status.Token) require.NotEqual(t, credOutput2.Status.Token, credOutput3.Status.Token)
// the logs contain only the ExecCredential. There are 2 elements because the last one is "".
require.Len(t, strings.Split(string(cmd3Output), "\n"), 2)
t.Logf("starting fourth CLI subprocess to test debug logging")
err = os.Setenv("PINNIPED_DEBUG", "true")
require.NoError(t, err)
command := oidcLoginCommand(ctx, t, pinnipedExe, sessionCachePath)
cmd4CombinedOutput, err := command.CombinedOutput()
cmd4StringOutput := string(cmd4CombinedOutput)
require.NoError(t, err, cmd4StringOutput)
// the logs contain only the 4 debug lines plus the ExecCredential. There are 6 elements because the last one is "".
require.Len(t, strings.Split(cmd4StringOutput, "\n"), 6)
require.Contains(t, cmd4StringOutput, "Performing OIDC login")
require.Contains(t, cmd4StringOutput, "Found unexpired cached token")
require.Contains(t, cmd4StringOutput, "No concierge configured, skipping token credential exchange")
require.Contains(t, cmd4StringOutput, "caching cluster credential for future use.")
require.Contains(t, cmd4StringOutput, credOutput3.Status.Token)
err = os.Unsetenv("PINNIPED_DEBUG")
require.NoError(t, err)
} }
func runPinnipedLoginOIDC( func runPinnipedLoginOIDC(
@ -271,6 +293,7 @@ func runPinnipedLoginOIDC(
// Start a background goroutine to read stderr from the CLI and parse out the login URL. // Start a background goroutine to read stderr from the CLI and parse out the login URL.
loginURLChan := make(chan string) loginURLChan := make(chan string)
spawnTestGoroutine(t, func() (err error) { spawnTestGoroutine(t, func() (err error) {
t.Helper()
defer func() { defer func() {
closeErr := stderr.Close() closeErr := stderr.Close()
if closeErr == nil || errors.Is(closeErr, os.ErrClosed) { if closeErr == nil || errors.Is(closeErr, os.ErrClosed) {
@ -282,16 +305,18 @@ func runPinnipedLoginOIDC(
}() }()
reader := bufio.NewReader(library.NewLoggerReader(t, "stderr", stderr)) reader := bufio.NewReader(library.NewLoggerReader(t, "stderr", stderr))
line, err := reader.ReadString('\n')
if err != nil { scanner := bufio.NewScanner(reader)
return fmt.Errorf("could not read login URL line from stderr: %w", err)
}
const prompt = "Please log in: " const prompt = "Please log in: "
if !strings.HasPrefix(line, prompt) { for scanner.Scan() {
return fmt.Errorf("expected %q to have prefix %q", line, prompt) line := scanner.Text()
if strings.HasPrefix(line, prompt) {
loginURLChan <- strings.TrimPrefix(line, prompt)
return nil
}
} }
loginURLChan <- strings.TrimPrefix(line, prompt)
return readAndExpectEmpty(reader) return fmt.Errorf("expected stderr to contain %s", prompt)
}) })
// Start a background goroutine to read stdout from the CLI and parse out an ExecCredential. // Start a background goroutine to read stdout from the CLI and parse out an ExecCredential.