Merge branch 'main' into main

This commit is contained in:
anjalitelang 2022-06-02 17:25:43 -04:00 committed by GitHub
commit 225bbdd36b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 5793 additions and 1836 deletions

View File

@ -8,22 +8,23 @@
package main package main
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog/v2"
concierge "go.pinniped.dev/internal/concierge/server"
// this side effect import ensures that we use fipsonly crypto in fips_strict mode. // this side effect import ensures that we use fipsonly crypto in fips_strict mode.
concierge "go.pinniped.dev/internal/concierge/server"
_ "go.pinniped.dev/internal/crypto/ptls" _ "go.pinniped.dev/internal/crypto/ptls"
lua "go.pinniped.dev/internal/localuserauthenticator" lua "go.pinniped.dev/internal/localuserauthenticator"
"go.pinniped.dev/internal/plog"
supervisor "go.pinniped.dev/internal/supervisor/server" supervisor "go.pinniped.dev/internal/supervisor/server"
) )
//nolint: gochecknoglobals // these are swapped during unit tests. // nolint: gochecknoglobals // these are swapped during unit tests.
var ( var (
fail = klog.Fatalf fail = plog.Fatal
subcommands = map[string]func(){ subcommands = map[string]func(){
"pinniped-concierge": concierge.Main, "pinniped-concierge": concierge.Main,
"pinniped-supervisor": supervisor.Main, "pinniped-supervisor": supervisor.Main,
@ -33,11 +34,11 @@ var (
func main() { func main() {
if len(os.Args) == 0 { if len(os.Args) == 0 {
fail("missing os.Args") fail(fmt.Errorf("missing os.Args"))
} }
binary := filepath.Base(os.Args[0]) binary := filepath.Base(os.Args[0])
if subcommands[binary] == nil { if subcommands[binary] == nil {
fail("must be invoked as one of %v, not %q", sets.StringKeySet(subcommands).List(), binary) fail(fmt.Errorf("must be invoked as one of %v, not %q", sets.StringKeySet(subcommands).List(), binary))
} }
subcommands[binary]() subcommands[binary]()
} }

View File

@ -43,8 +43,11 @@ func TestEntrypoint(t *testing.T) {
var logBuf bytes.Buffer var logBuf bytes.Buffer
testLog := log.New(&logBuf, "", 0) testLog := log.New(&logBuf, "", 0)
exited := "exiting via fatal" exited := "exiting via fatal"
fail = func(format string, v ...interface{}) { fail = func(err error, keysAndValues ...interface{}) {
testLog.Printf(format, v...) testLog.Print(err)
if len(keysAndValues) > 0 {
testLog.Print(keysAndValues...)
}
panic(exited) panic(exited)
} }

View File

@ -11,7 +11,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
@ -19,8 +18,6 @@ import (
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/go-logr/logr"
"github.com/go-logr/stdr"
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
@ -34,23 +31,24 @@ import (
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
"go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/plog"
) )
type kubeconfigDeps struct { type kubeconfigDeps struct {
getPathToSelf func() (string, error) getPathToSelf func() (string, error)
getClientset getConciergeClientsetFunc getClientset getConciergeClientsetFunc
log logr.Logger log plog.MinLogger
} }
func kubeconfigRealDeps() kubeconfigDeps { func kubeconfigRealDeps() kubeconfigDeps {
return kubeconfigDeps{ return kubeconfigDeps{
getPathToSelf: os.Executable, getPathToSelf: os.Executable,
getClientset: getRealConciergeClientset, getClientset: getRealConciergeClientset,
log: stdr.New(log.New(os.Stderr, "", 0)), log: plog.New(),
} }
} }
//nolint: gochecknoinits // nolint: gochecknoinits
func init() { func init() {
getCmd.AddCommand(kubeconfigCommand(kubeconfigRealDeps())) getCmd.AddCommand(kubeconfigCommand(kubeconfigRealDeps()))
} }
@ -175,6 +173,11 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f
ctx, cancel := context.WithTimeout(ctx, flags.timeout) ctx, cancel := context.WithTimeout(ctx, flags.timeout)
defer cancel() defer cancel()
// the log statements in this file assume that Info logs are unconditionally printed so we set the global level to info
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelInfo, Format: plog.FormatCLI}); err != nil {
return err
}
// Validate api group suffix and immediately return an error if it is invalid. // Validate api group suffix and immediately return an error if it is invalid.
if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil { if err := groupsuffix.Validate(flags.concierge.apiGroupSuffix); err != nil {
return fmt.Errorf("invalid API group suffix: %w", err) return fmt.Errorf("invalid API group suffix: %w", err)
@ -233,7 +236,7 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f
// When all the upstream IDP flags are set by the user, then skip discovery and don't validate their input. Maybe they know something // When all the upstream IDP flags are set by the user, then skip discovery and don't validate their input. Maybe they know something
// that we can't know, like the name of an IDP that they are going to define in the future. // that we can't know, like the name of an IDP that they are going to define in the future.
if len(flags.oidc.issuer) > 0 && (flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "" || flags.oidc.upstreamIDPFlow == "") { if len(flags.oidc.issuer) > 0 && (flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "" || flags.oidc.upstreamIDPFlow == "") {
if err := discoverSupervisorUpstreamIDP(ctx, &flags); err != nil { if err := discoverSupervisorUpstreamIDP(ctx, &flags, deps.log); err != nil {
return err return err
} }
} }
@ -398,7 +401,7 @@ func waitForCredentialIssuer(ctx context.Context, clientset conciergeclientset.I
return credentialIssuer, nil return credentialIssuer, nil
} }
func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log logr.Logger) error { func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer, flags *getKubeconfigParams, v1Cluster *clientcmdapi.Cluster, log plog.MinLogger) error {
// Autodiscover the --concierge-mode. // Autodiscover the --concierge-mode.
frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode) frontend, err := getConciergeFrontend(credentialIssuer, flags.concierge.mode)
if err != nil { if err != nil {
@ -446,7 +449,7 @@ func discoverConciergeParams(credentialIssuer *configv1alpha1.CredentialIssuer,
return nil return nil
} }
func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.Logger) { func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log plog.MinLogger) {
for _, strategy := range credentialIssuer.Status.Strategies { for _, strategy := range credentialIssuer.Status.Strategies {
log.Info("found CredentialIssuer strategy", log.Info("found CredentialIssuer strategy",
"type", strategy.Type, "type", strategy.Type,
@ -457,7 +460,7 @@ func logStrategies(credentialIssuer *configv1alpha1.CredentialIssuer, log logr.L
} }
} }
func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log logr.Logger) error { func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log plog.MinLogger) error {
switch auth := authenticator.(type) { switch auth := authenticator.(type) {
case *conciergev1alpha1.WebhookAuthenticator: case *conciergev1alpha1.WebhookAuthenticator:
// If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set // If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set
@ -556,7 +559,7 @@ func newExecKubeconfig(cluster *clientcmdapi.Cluster, execConfig *clientcmdapi.E
} }
} }
func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log logr.Logger) (*configv1alpha1.CredentialIssuer, error) { func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string, log plog.MinLogger) (*configv1alpha1.CredentialIssuer, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc() defer cancelFunc()
@ -582,7 +585,7 @@ func lookupCredentialIssuer(clientset conciergeclientset.Interface, name string,
return result, nil return result, nil
} }
func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log logr.Logger) (metav1.Object, error) { func lookupAuthenticator(clientset conciergeclientset.Interface, authType, authName string, log plog.MinLogger) (metav1.Object, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc() defer cancelFunc()
@ -643,7 +646,7 @@ func writeConfigAsYAML(out io.Writer, config clientcmdapi.Config) error {
return nil return nil
} }
func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log logr.Logger) error { func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconfig clientcmdapi.Config, log plog.MinLogger) error {
if flags.skipValidate { if flags.skipValidate {
return nil return nil
} }
@ -706,7 +709,7 @@ func validateKubeconfig(ctx context.Context, flags getKubeconfigParams, kubeconf
log.Info("validated connection to the cluster", "attempts", attempts) log.Info("validated connection to the cluster", "attempts", attempts)
return nil return nil
} }
log.Error(err, "could not connect to cluster, retrying...", "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String()) log.Info("could not connect to cluster, retrying...", "error", err, "attempts", attempts, "remaining", time.Until(deadline).Round(time.Second).String())
} }
} }
} }
@ -726,7 +729,7 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool
return false return false
} }
func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error { func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams, log plog.MinLogger) error {
httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle) httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle)
if err != nil { if err != nil {
return err return err
@ -758,7 +761,7 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigPara
return err return err
} }
selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow) selectedIDPFlow, err := selectUpstreamIDPFlow(discoveredIDPFlows, selectedIDPName, selectedIDPType, flags.oidc.upstreamIDPFlow, log)
if err != nil { if err != nil {
return err return err
} }
@ -898,7 +901,7 @@ func selectUpstreamIDPNameAndType(pinnipedIDPs []idpdiscoveryv1alpha1.PinnipedID
} }
} }
func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string) (idpdiscoveryv1alpha1.IDPFlow, error) { func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, selectedIDPName string, selectedIDPType idpdiscoveryv1alpha1.IDPType, specifiedFlow string, log plog.MinLogger) (idpdiscoveryv1alpha1.IDPFlow, error) {
switch { switch {
case len(discoveredIDPFlows) == 0: case len(discoveredIDPFlows) == 0:
// No flows listed by discovery means that we are talking to an old Supervisor from before this feature existed. // No flows listed by discovery means that we are talking to an old Supervisor from before this feature existed.
@ -922,10 +925,9 @@ func selectUpstreamIDPFlow(discoveredIDPFlows []idpdiscoveryv1alpha1.IDPFlow, se
return discoveredIDPFlows[0], nil return discoveredIDPFlows[0], nil
default: default:
// The user did not specify a flow, and more than one was found. // The user did not specify a flow, and more than one was found.
return "", fmt.Errorf( log.Info("multiple client flows found, selecting first value as default",
"multiple client flows for Supervisor upstream identity provider %q of type %q were found, "+ "idpName", selectedIDPName, "idpType", selectedIDPType,
"so the --upstream-identity-provider-flow flag must be specified. "+ "selectedFlow", discoveredIDPFlows[0].String(), "availableFlows", discoveredIDPFlows)
"Found these flows: %v", return discoveredIDPFlows[0], nil
selectedIDPName, selectedIDPType, discoveredIDPFlows)
} }
} }

View File

@ -1261,13 +1261,52 @@ func TestGetKubeconfig(t *testing.T) {
oidcDiscoveryResponse: happyOIDCDiscoveryResponse, oidcDiscoveryResponse: happyOIDCDiscoveryResponse,
idpsDiscoveryResponse: here.Docf(`{ idpsDiscoveryResponse: here.Docf(`{
"pinniped_identity_providers": [ "pinniped_identity_providers": [
{"name": "some-oidc-idp", "type": "oidc", "flows": ["flow1", "flow2"]} {"name": "some-ldap-idp", "type": "ldap", "flows": ["cli_password", "flow2"]}
] ]
}`), }`),
wantError: true, wantStdout: func(issuerCABundle string, issuerURL string) string {
wantStderr: func(issuerCABundle string, issuerURL string) string { return here.Docf(`
return `Error: multiple client flows for Supervisor upstream identity provider "some-oidc-idp" of type "oidc" were found, so the --upstream-identity-provider-flow flag must be specified.` + apiVersion: v1
` Found these flows: [flow1 flow2]` + "\n" clusters:
- cluster:
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
server: https://fake-server-url-value
name: kind-cluster-pinniped
contexts:
- context:
cluster: kind-cluster-pinniped
user: kind-user-pinniped
name: kind-context-pinniped
current-context: kind-context-pinniped
kind: Config
preferences: {}
users:
- name: kind-user-pinniped
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
args:
- login
- oidc
- --issuer=%s
- --client-id=pinniped-cli
- --scopes=offline_access,openid,pinniped:request-audience
- --ca-bundle-data=%s
- --upstream-identity-provider-name=some-ldap-idp
- --upstream-identity-provider-type=ldap
- --upstream-identity-provider-flow=cli_password
command: '.../path/to/pinniped'
env: []
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
for more details
provideClusterInfo: true
`,
issuerURL,
base64.StdEncoding.EncodeToString([]byte(issuerCABundle)))
},
wantLogs: func(_ string, _ string) []string {
return []string{`"level"=0 "msg"="multiple client flows found, selecting first value as default" ` +
`"availableFlows"=["cli_password","flow2"] "idpName"="some-ldap-idp" "idpType"="ldap" "selectedFlow"="cli_password"`}
}, },
}, },
{ {
@ -2850,7 +2889,7 @@ func TestGetKubeconfig(t *testing.T) {
}) })
issuerEndpointPtr = &issuerEndpoint issuerEndpointPtr = &issuerEndpoint
testLog := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements testLog := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements
cmd := kubeconfigCommand(kubeconfigDeps{ cmd := kubeconfigCommand(kubeconfigDeps{
getPathToSelf: func() (string, error) { getPathToSelf: func() (string, error) {
if tt.getPathToSelfErr != nil { if tt.getPathToSelfErr != nil {

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package cmd package cmd
@ -20,7 +20,6 @@ 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/klog/v2/klogr"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/internal/execcredcache" "go.pinniped.dev/internal/execcredcache"
@ -33,7 +32,7 @@ import (
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
) )
//nolint: gochecknoinits // nolint: gochecknoinits
func init() { func init() {
loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps())) loginCmd.AddCommand(oidcLoginCommand(oidcLoginCommandRealDeps()))
} }
@ -125,7 +124,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
} }
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen
pLogger, err := SetLogLevel(deps.lookupEnv) pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv)
if err != nil { if err != nil {
plog.WarningErr("Received error while setting log level", err) plog.WarningErr("Received error while setting log level", err)
} }
@ -133,11 +132,11 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin
// Initialize the session cache. // Initialize the session cache.
var sessionOptions []filesession.Option var sessionOptions []filesession.Option
// If the hidden --debug-session-cache option is passed, log all the errors from the session cache with klog. // If the hidden --debug-session-cache option is passed, log all the errors from the session cache.
if flags.debugSessionCache { if flags.debugSessionCache {
logger := klogr.New().WithName("session") logger := plog.WithName("session")
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) { sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
logger.Error(err, "error during session cache operation") logger.Error("error during session cache operation", err)
})) }))
} }
sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...) sessionCache := filesession.New(flags.sessionCachePath, sessionOptions...)
@ -145,7 +144,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.WithLogger(plog.Logr()), // nolint: staticcheck // old code with lots of log statements
oidcclient.WithScopes(flags.scopes), oidcclient.WithScopes(flags.scopes),
oidcclient.WithSessionCache(sessionCache), oidcclient.WithSessionCache(sessionCache),
} }
@ -271,11 +270,11 @@ func flowOptions(requestedIDPType idpdiscoveryv1alpha1.IDPType, requestedFlow id
case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "": case idpdiscoveryv1alpha1.IDPFlowCLIPassword, "":
return useCLIFlow, nil return useCLIFlow, nil
case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode: case idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode:
fallthrough // not supported for LDAP providers, so fallthrough to error case return nil, nil
default: default:
return nil, fmt.Errorf( return nil, fmt.Errorf(
"--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)", "--upstream-identity-provider-flow value not recognized for identity provider type %q: %s (supported values: %s)",
requestedIDPType, requestedFlow, []string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String()}) requestedIDPType, requestedFlow, strings.Join([]string{idpdiscoveryv1alpha1.IDPFlowCLIPassword.String(), idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode.String()}, ", "))
} }
default: default:
// Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236 // Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236
@ -326,15 +325,15 @@ func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
return &cred return &cred
} }
func SetLogLevel(lookupEnv func(string) (string, bool)) (plog.Logger, error) { func SetLogLevel(ctx context.Context, lookupEnv func(string) (string, bool)) (plog.Logger, error) {
debug, _ := lookupEnv("PINNIPED_DEBUG") debug, _ := lookupEnv("PINNIPED_DEBUG")
if debug == "true" { if debug == "true" {
err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug) err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug, Format: plog.FormatCLI})
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
logger := plog.New("Pinniped login: ") logger := plog.New().WithName("pinniped-login")
return logger, nil return logger, nil
} }

View File

@ -10,18 +10,20 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap"
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" clocktesting "k8s.io/utils/clock/testing"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog"
"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"
@ -38,6 +40,10 @@ func TestLoginOIDCCommand(t *testing.T) {
time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC) time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC)
now, err := time.Parse(time.RFC3339Nano, "2028-10-11T23:37:26.953313745Z")
require.NoError(t, err)
nowStr := now.Local().Format(time.RFC1123)
tests := []struct { tests := []struct {
name string name string
args []string args []string
@ -235,18 +241,30 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 5, 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", 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 unsupported flow is an error", name: "ldap upstream type with unsupported flow is an error",
args: []string{ args: []string{
"--issuer", "test-issuer", "--issuer", "test-issuer",
"--client-id", "test-client-id", "--client-id", "test-client-id",
"--upstream-identity-provider-type", "ldap", "--upstream-identity-provider-type", "ldap",
"--upstream-identity-provider-flow", "browser_authcode", // "browser_authcode" is only supported for OIDC upstreams "--upstream-identity-provider-flow", "foo",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
}, },
wantError: true, wantError: true,
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": browser_authcode (supported values: [cli_password]) Error: --upstream-identity-provider-flow value not recognized for identity provider type "ldap": foo (supported values: cli_password, browser_authcode)
`), `),
}, },
{ {
@ -261,18 +279,30 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 5, 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", 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 unsupported flow is an error", name: "active directory upstream type with unsupported flow is an error",
args: []string{ args: []string{
"--issuer", "test-issuer", "--issuer", "test-issuer",
"--client-id", "test-client-id", "--client-id", "test-client-id",
"--upstream-identity-provider-type", "activedirectory", "--upstream-identity-provider-type", "activedirectory",
"--upstream-identity-provider-flow", "browser_authcode", // "browser_authcode" is only supported for OIDC upstreams "--upstream-identity-provider-flow", "foo",
"--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution
}, },
wantError: true, wantError: true,
wantStderr: here.Doc(` wantStderr: here.Doc(`
Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": browser_authcode (supported values: [cli_password]) Error: --upstream-identity-provider-flow value not recognized for identity provider type "activedirectory": foo (supported values: cli_password, browser_authcode)
`), `),
}, },
{ {
@ -318,8 +348,8 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 4, 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", 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{ wantLogs: []string{
"\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"", nowStr + ` pinniped-login cmd/login_oidc.go:222 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
"\"level\"=0 \"msg\"=\"Pinniped login: No concierge configured, skipping token credential exchange\"", nowStr + ` pinniped-login cmd/login_oidc.go:242 No concierge configured, skipping token credential exchange`,
}, },
}, },
{ {
@ -348,18 +378,20 @@ func TestLoginOIDCCommand(t *testing.T) {
wantOptionsCount: 11, wantOptionsCount: 11,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n", wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n",
wantLogs: []string{ wantLogs: []string{
"\"level\"=0 \"msg\"=\"Pinniped login: Performing OIDC login\" \"client id\"=\"test-client-id\" \"issuer\"=\"test-issuer\"", nowStr + ` pinniped-login cmd/login_oidc.go:222 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`,
"\"level\"=0 \"msg\"=\"Pinniped login: Exchanging token for cluster credential\" \"authenticator name\"=\"test-authenticator\" \"authenticator type\"=\"webhook\" \"endpoint\"=\"https://127.0.0.1:1234/\"", nowStr + ` pinniped-login cmd/login_oidc.go:232 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
"\"level\"=0 \"msg\"=\"Pinniped login: Successfully exchanged token for cluster credential.\"", nowStr + ` pinniped-login cmd/login_oidc.go:240 Successfully exchanged token for cluster credential.`,
"\"level\"=0 \"msg\"=\"Pinniped login: caching cluster credential for future use.\"", nowStr + ` pinniped-login cmd/login_oidc.go:247 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.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements var buf bytes.Buffer
klog.SetLogger(testLogger.Logger) fakeClock := clocktesting.NewFakeClock(now)
ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock)))
var ( var (
gotOptions []oidcclient.Option gotOptions []oidcclient.Option
) )
@ -404,7 +436,7 @@ func TestLoginOIDCCommand(t *testing.T) {
cmd.SetOut(&stdout) cmd.SetOut(&stdout)
cmd.SetErr(&stderr) cmd.SetErr(&stderr)
cmd.SetArgs(tt.args) cmd.SetArgs(tt.args)
err := cmd.Execute() err = cmd.ExecuteContext(ctx)
if tt.wantError { if tt.wantError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -414,7 +446,15 @@ func TestLoginOIDCCommand(t *testing.T) {
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()) require.Equal(t, tt.wantLogs, logLines(buf.String()))
}) })
} }
} }
func logLines(logs string) []string {
if len(logs) == 0 {
return nil
}
return strings.Split(strings.TrimSpace(logs), "\n")
}

View File

@ -7,7 +7,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -22,7 +21,7 @@ import (
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
) )
//nolint: gochecknoinits // nolint: gochecknoinits
func init() { func init() {
loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps())) loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps()))
} }
@ -75,7 +74,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix")
cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)") cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)")
cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) } cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd, deps, flags) }
mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore") mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore")
mustMarkHidden(cmd, "concierge-namespace") mustMarkHidden(cmd, "concierge-namespace")
@ -83,8 +82,9 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
return cmd return cmd
} }
func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error { func runStaticLogin(cmd *cobra.Command, deps staticLoginDeps, flags staticLoginParams) error {
pLogger, err := SetLogLevel(deps.lookupEnv) out := cmd.OutOrStdout()
pLogger, err := SetLogLevel(cmd.Context(), deps.lookupEnv)
if err != nil { if err != nil {
plog.WarningErr("Received error while setting log level", err) plog.WarningErr("Received error while setting log level", err)
} }

View File

@ -12,16 +12,15 @@ 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"
"go.uber.org/zap"
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"
clocktesting "k8s.io/utils/clock/testing"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/conciergeclient"
) )
@ -35,6 +34,10 @@ func TestLoginStaticCommand(t *testing.T) {
testCABundlePath := filepath.Join(tmpdir, "testca.pem") testCABundlePath := filepath.Join(tmpdir, "testca.pem")
require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600)) require.NoError(t, ioutil.WriteFile(testCABundlePath, testCA.Bundle(), 0600))
now, err := time.Parse(time.RFC3339Nano, "2038-12-07T23:37:26.953313745Z")
require.NoError(t, err)
nowStr := now.Local().Format(time.RFC1123)
tests := []struct { tests := []struct {
name string name string
args []string args []string
@ -136,7 +139,9 @@ func TestLoginStaticCommand(t *testing.T) {
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/\""}, wantLogs: []string{
nowStr + ` pinniped-login cmd/login_static.go:147 exchanging static token for cluster credential {"endpoint": "https://127.0.0.1/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`,
},
}, },
{ {
name: "invalid API group suffix", name: "invalid API group suffix",
@ -165,8 +170,10 @@ func TestLoginStaticCommand(t *testing.T) {
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.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements var buf bytes.Buffer
klog.SetLogger(testLogger.Logger) fakeClock := clocktesting.NewFakeClock(now)
ctx := plog.TestZapOverrides(context.Background(), t, &buf, nil, zap.WithClock(plog.ZapClock(fakeClock)))
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]
@ -194,7 +201,7 @@ func TestLoginStaticCommand(t *testing.T) {
cmd.SetOut(&stdout) cmd.SetOut(&stdout)
cmd.SetErr(&stderr) cmd.SetErr(&stderr)
cmd.SetArgs(tt.args) cmd.SetArgs(tt.args)
err := cmd.Execute() err := cmd.ExecuteContext(ctx)
if tt.wantError { if tt.wantError {
require.Error(t, err) require.Error(t, err)
} else { } else {
@ -203,7 +210,7 @@ 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()) require.Equal(t, tt.wantLogs, logLines(buf.String()))
}) })
} }
} }

View File

@ -4,12 +4,14 @@
package cmd package cmd
import ( import (
"os" "context"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.pinniped.dev/internal/plog"
) )
//nolint: gochecknoglobals // nolint: gochecknoglobals
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "pinniped", Use: "pinniped",
Short: "pinniped", Short: "pinniped",
@ -19,8 +21,11 @@ var rootCmd = &cobra.Command{
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd. // This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() { func Execute() error {
if err := rootCmd.Execute(); err != nil { defer plog.Setup()()
os.Exit(1) // the context does not matter here because it is unused when CLI formatting is provided
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(context.Background(), plog.LogSpec{Format: plog.FormatCLI}); err != nil {
return err
} }
return rootCmd.Execute()
} }

View File

@ -13,7 +13,7 @@ import (
_ "go.pinniped.dev/internal/crypto/ptls" _ "go.pinniped.dev/internal/crypto/ptls"
) )
//nolint: gochecknoinits // nolint: gochecknoinits
func init() { func init() {
// browsers like chrome like to write to our std out which breaks our JSON ExecCredential output // browsers like chrome like to write to our std out which breaks our JSON ExecCredential output
// thus we redirect the browser's std out to our std err // thus we redirect the browser's std out to our std err
@ -21,5 +21,7 @@ func init() {
} }
func main() { func main() {
cmd.Execute() if err := cmd.Execute(); err != nil {
os.Exit(1)
}
} }

View File

@ -86,8 +86,14 @@ data:
imagePullSecrets: imagePullSecrets:
- image-pull-secret - image-pull-secret
(@ end @) (@ end @)
(@ if data.values.log_level or data.values.deprecated_log_format: @)
log:
(@ if data.values.log_level: @) (@ if data.values.log_level: @)
logLevel: (@= getAndValidateLogLevel() @) level: (@= getAndValidateLogLevel() @)
(@ end @)
(@ if data.values.deprecated_log_format: @)
format: (@= data.values.deprecated_log_format @)
(@ end @)
(@ end @) (@ end @)
--- ---
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "": #@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":

View File

@ -54,6 +54,10 @@ api_serving_certificate_renew_before_seconds: 2160000
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer #! Specify the verbosity of logging: info ("nice to know" information), debug (developer
#! information), trace (timing information), all (kitchen sink). #! information), trace (timing information), all (kitchen sink).
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs. log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
#! Specify the format of logging: json (for machine parsable logs) and text (for legacy klog formatted logs).
#! By default, when this value is left unset, logs are formatted in json.
#! This configuration is deprecated and will be removed in a future release at which point logs will always be formatted as json.
deprecated_log_format:
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice

View File

@ -54,8 +54,14 @@ _: #@ template.replace(data.values.custom_labels)
#@ "labels": labels(), #@ "labels": labels(),
#@ "insecureAcceptExternalUnencryptedHttpRequests": data.values.deprecated_insecure_accept_external_unencrypted_http_requests #@ "insecureAcceptExternalUnencryptedHttpRequests": data.values.deprecated_insecure_accept_external_unencrypted_http_requests
#@ } #@ }
#@ if data.values.log_level or data.values.deprecated_log_format:
#@ config["log"] = {}
#@ end
#@ if data.values.log_level: #@ if data.values.log_level:
#@ config["logLevel"] = getAndValidateLogLevel() #@ config["log"]["level"] = getAndValidateLogLevel()
#@ end
#@ if data.values.deprecated_log_format:
#@ config["log"]["format"] = data.values.deprecated_log_format
#@ end #@ end
#@ if data.values.endpoints: #@ if data.values.endpoints:
#@ config["endpoints"] = data.values.endpoints #@ config["endpoints"] = data.values.endpoints

View File

@ -57,6 +57,10 @@ service_loadbalancer_ip: #! e.g. 1.2.3.4
#! Specify the verbosity of logging: info ("nice to know" information), debug (developer information), trace (timing information), #! Specify the verbosity of logging: info ("nice to know" information), debug (developer information), trace (timing information),
#! or all (kitchen sink). Do not use trace or all on production systems, as credentials may get logged. #! or all (kitchen sink). Do not use trace or all on production systems, as credentials may get logged.
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs. log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
#! Specify the format of logging: json (for machine parsable logs) and text (for legacy klog formatted logs).
#! By default, when this value is left unset, logs are formatted in json.
#! This configuration is deprecated and will be removed in a future release at which point logs will always be formatted as json.
deprecated_log_format:
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice

3
go.mod
View File

@ -45,6 +45,7 @@ require (
github.com/go-ldap/ldap/v3 v3.4.3 github.com/go-ldap/ldap/v3 v3.4.3
github.com/go-logr/logr v1.2.3 github.com/go-logr/logr v1.2.3
github.com/go-logr/stdr v1.2.2 github.com/go-logr/stdr v1.2.2
github.com/go-logr/zapr v1.2.3
github.com/gofrs/flock v0.8.1 github.com/gofrs/flock v0.8.1
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.8 github.com/google/go-cmp v0.5.8
@ -64,6 +65,7 @@ require (
github.com/stretchr/testify v1.7.1 github.com/stretchr/testify v1.7.1
github.com/tdewolff/minify/v2 v2.11.2 github.com/tdewolff/minify/v2 v2.11.2
go.uber.org/atomic v1.9.0 go.uber.org/atomic v1.9.0
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
@ -168,7 +170,6 @@ require (
go.opentelemetry.io/otel/trace v1.6.3 // indirect go.opentelemetry.io/otel/trace v1.6.3 // indirect
go.opentelemetry.io/proto/otlp v0.15.0 // indirect go.opentelemetry.io/proto/otlp v0.15.0 // indirect
go.uber.org/multierr v1.8.0 // indirect go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect

2
go.sum
View File

@ -299,6 +299,8 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro=
github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=

View File

@ -25,11 +25,36 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT" cd "$ROOT"
function log_error() {
RED='\033[0;31m'
NC='\033[0m'
if [[ ${COLORTERM:-unknown} =~ ^(truecolor|24bit)$ ]]; then
echo -e "🙁${RED} Error: $* ${NC}"
else
echo ":( Error: $*"
fi
}
use_oidc_upstream=no use_oidc_upstream=no
use_ldap_upstream=no use_ldap_upstream=no
use_ad_upstream=no use_ad_upstream=no
use_flow=""
while (("$#")); do while (("$#")); do
case "$1" in case "$1" in
--flow)
shift
# If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error
if [[ "$#" == "0" || "$1" == -* ]]; then
log_error "--flow requires a flow name to be specified (e.g. cli_password or browser_authcode"
exit 1
fi
if [[ "$1" != "browser_authcode" && "$1" != "cli_password" ]]; then
log_error "--flow must be cli_password or browser_authcode"
exit 1
fi
use_flow=$1
shift
;;
--ldap) --ldap)
use_ldap_upstream=yes use_ldap_upstream=yes
shift shift
@ -56,7 +81,7 @@ while (("$#")); do
done done
if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" && "$use_ad_upstream" == "no" ]]; then if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" && "$use_ad_upstream" == "no" ]]; then
echo "Error: Please use --oidc, --ldap, or --ad to specify which type of upstream identity provider(s) you would like" log_error "Error: Please use --oidc, --ldap, or --ad to specify which type of upstream identity provider(s) you would like"
exit 1 exit 1
fi fi
@ -127,6 +152,7 @@ spec:
certificateAuthorityData: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE" certificateAuthorityData: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE"
authorizationConfig: authorizationConfig:
additionalScopes: [ ${PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES} ] additionalScopes: [ ${PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES} ]
allowPasswordGrant: true
claims: claims:
username: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM" username: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM"
groups: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM" groups: "$PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM"
@ -196,7 +222,7 @@ EOF
fi fi
if [[ "$use_ad_upstream" == "yes" ]]; then if [[ "$use_ad_upstream" == "yes" ]]; then
# Make an ActiveDirectoryIdentityProvider. # Make an ActiveDirectoryIdentityProvider. Needs to be pointed to a real AD server by env vars.
cat <<EOF | kubectl apply --namespace "$PINNIPED_TEST_SUPERVISOR_NAMESPACE" -f - cat <<EOF | kubectl apply --namespace "$PINNIPED_TEST_SUPERVISOR_NAMESPACE" -f -
apiVersion: idp.supervisor.pinniped.dev/v1alpha1 apiVersion: idp.supervisor.pinniped.dev/v1alpha1
kind: ActiveDirectoryIdentityProvider kind: ActiveDirectoryIdentityProvider
@ -256,7 +282,11 @@ while [[ -z "$(kubectl get credentialissuer pinniped-concierge-config -o=jsonpat
done done
# Use the CLI to get the kubeconfig. Tell it that you don't want the browser to automatically open for logins. # Use the CLI to get the kubeconfig. Tell it that you don't want the browser to automatically open for logins.
https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" ./pinniped get kubeconfig --oidc-skip-browser >kubeconfig flow_arg=""
if [[ -n "$use_flow" ]]; then
flow_arg="--upstream-identity-provider-flow $use_flow"
fi
https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" ./pinniped get kubeconfig --oidc-skip-browser $flow_arg >kubeconfig
# Clear the local CLI cache to ensure that the kubectl command below will need to perform a fresh login. # Clear the local CLI cache to ensure that the kubectl command below will need to perform a fresh login.
rm -f "$HOME/.config/pinniped/sessions.yaml" rm -f "$HOME/.config/pinniped/sessions.yaml"
@ -265,25 +295,27 @@ rm -f "$HOME/.config/pinniped/credentials.yaml"
echo echo
echo "Ready! 🚀" echo "Ready! 🚀"
if [[ "$use_oidc_upstream" == "yes" ]]; then if [[ "$use_oidc_upstream" == "yes" || "$use_flow" == "browser_authcode" ]]; then
echo echo
echo "To be able to access the login URL shown below, start Chrome like this:" echo "To be able to access the login URL shown below, start Chrome like this:"
echo " open -a \"Google Chrome\" --args --proxy-server=\"$PINNIPED_TEST_PROXY\"" echo " open -a \"Google Chrome\" --args --proxy-server=\"$PINNIPED_TEST_PROXY\""
echo "Then use these credentials at the Dex login page:" echo "Note that Chrome must be fully quit before being started with --proxy-server."
echo "Then open the login URL shown below in that new Chrome window."
echo
echo "When prompted for username and password, use these values:"
fi
if [[ "$use_oidc_upstream" == "yes" ]]; then
echo " Username: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME" echo " Username: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME"
echo " Password: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD" echo " Password: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD"
fi fi
if [[ "$use_ldap_upstream" == "yes" ]]; then if [[ "$use_ldap_upstream" == "yes" ]]; then
echo
echo "When prompted for username and password by the CLI, use these values:"
echo " Username: $PINNIPED_TEST_LDAP_USER_CN" echo " Username: $PINNIPED_TEST_LDAP_USER_CN"
echo " Password: $PINNIPED_TEST_LDAP_USER_PASSWORD" echo " Password: $PINNIPED_TEST_LDAP_USER_PASSWORD"
fi fi
if [[ "$use_ad_upstream" == "yes" ]]; then if [[ "$use_ad_upstream" == "yes" ]]; then
echo
echo "When prompted for username and password by the CLI, use these values:"
echo " Username: $PINNIPED_TEST_AD_USER_USER_PRINCIPAL_NAME" echo " Username: $PINNIPED_TEST_AD_USER_USER_PRINCIPAL_NAME"
echo " Password: $PINNIPED_TEST_AD_USER_PASSWORD" echo " Password: $PINNIPED_TEST_AD_USER_PASSWORD"
fi fi

View File

@ -15,12 +15,11 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
apimachineryversion "k8s.io/apimachinery/pkg/version"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options" genericoptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/pkg/version" "k8s.io/client-go/pkg/version"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/certauthority/dynamiccertauthority" "go.pinniped.dev/internal/certauthority/dynamiccertauthority"
"go.pinniped.dev/internal/concierge/apiserver" "go.pinniped.dev/internal/concierge/apiserver"
@ -35,6 +34,7 @@ import (
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/issuer" "go.pinniped.dev/internal/issuer"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/registry/credentialrequest" "go.pinniped.dev/internal/registry/credentialrequest"
) )
@ -100,7 +100,7 @@ func addCommandlineFlagsToCommand(cmd *cobra.Command, app *App) {
// Boot the aggregated API server, which will in turn boot the controllers. // Boot the aggregated API server, which will in turn boot the controllers.
func (a *App) runServer(ctx context.Context) error { func (a *App) runServer(ctx context.Context) error {
// Read the server config file. // Read the server config file.
cfg, err := concierge.FromPath(a.configPath) cfg, err := concierge.FromPath(ctx, a.configPath)
if err != nil { if err != nil {
return fmt.Errorf("could not load config: %w", err) return fmt.Errorf("could not load config: %w", err)
} }
@ -250,16 +250,20 @@ func getAggregatedAPIServerConfig(
return apiServerConfig, nil return apiServerConfig, nil
} }
func main() error { // return an error instead of klog.Fatal to allow defer statements to run func main() error { // return an error instead of plog.Fatal to allow defer statements to run
logs.InitLogs() defer plog.Setup()()
defer logs.FlushLogs()
// Dump out the time since compile (mostly useful for benchmarking our local development cycle latency). // Dump out the time since compile (mostly useful for benchmarking our local development cycle latency).
var timeSinceCompile time.Duration var timeSinceCompile time.Duration
if buildDate, err := time.Parse(time.RFC3339, version.Get().BuildDate); err == nil { if buildDate, err := time.Parse(time.RFC3339, version.Get().BuildDate); err == nil {
timeSinceCompile = time.Since(buildDate).Round(time.Second) timeSinceCompile = time.Since(buildDate).Round(time.Second)
} }
klog.Infof("Running %s at %#v (%s since build)", rest.DefaultKubernetesUserAgent(), version.Get(), timeSinceCompile)
plog.Always("Running concierge",
"user-agent", rest.DefaultKubernetesUserAgent(),
"version", versionInfo(version.Get()),
"time-since-build", timeSinceCompile,
)
ctx := genericapiserver.SetupSignalContext() ctx := genericapiserver.SetupSignalContext()
@ -268,6 +272,8 @@ func main() error { // return an error instead of klog.Fatal to allow defer stat
func Main() { func Main() {
if err := main(); err != nil { if err := main(); err != nil {
klog.Fatal(err) plog.Fatal(err)
} }
} }
type versionInfo apimachineryversion.Info // hide .String() method from plog

View File

@ -6,6 +6,7 @@
package concierge package concierge
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strings" "strings"
@ -41,7 +42,7 @@ const (
// Note! The Config file should contain base64-encoded WebhookCABundle data. // Note! The Config file should contain base64-encoded WebhookCABundle data.
// This function will decode that base64-encoded data to PEM bytes to be stored // This function will decode that base64-encoded data to PEM bytes to be stored
// in the Config. // in the Config.
func FromPath(path string) (*Config, error) { func FromPath(ctx context.Context, path string) (*Config, error) {
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("read file: %w", err) return nil, fmt.Errorf("read file: %w", err)
@ -78,7 +79,8 @@ func FromPath(path string) (*Config, error) {
return nil, fmt.Errorf("validate names: %w", err) return nil, fmt.Errorf("validate names: %w", err)
} }
if err := plog.ValidateAndSetLogLevelGlobally(config.LogLevel); err != nil { plog.MaybeSetDeprecatedLogLevel(config.LogLevel, &config.Log)
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, config.Log); err != nil {
return nil, fmt.Errorf("validate log level: %w", err) return nil, fmt.Errorf("validate log level: %w", err)
} }

View File

@ -4,6 +4,7 @@
package concierge package concierge
import ( import (
"context"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
@ -90,9 +91,186 @@ func TestFromPath(t *testing.T) {
Image: pointer.StringPtr("kube-cert-agent-image"), Image: pointer.StringPtr("kube-cert-agent-image"),
ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"},
}, },
LogLevel: plog.LevelDebug, LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelDebug),
Log: plog.LogSpec{
Level: plog.LevelDebug,
}, },
}, },
},
{
name: "Fully filled out new log struct",
yaml: here.Doc(`
---
discovery:
url: https://some.discovery/url
api:
servingCertificate:
durationSeconds: 3600
renewBeforeSeconds: 2400
apiGroupSuffix: some.suffix.com
aggregatedAPIServerPort: 12345
impersonationProxyServerPort: 4242
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
apiService: pinniped-api
kubeCertAgentPrefix: kube-cert-agent-prefix
impersonationLoadBalancerService: impersonationLoadBalancerService-value
impersonationClusterIPService: impersonationClusterIPService-value
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
impersonationCACertificateSecret: impersonationCACertificateSecret-value
impersonationSignerSecret: impersonationSignerSecret-value
impersonationSignerSecret: impersonationSignerSecret-value
agentServiceAccount: agentServiceAccount-value
extraName: extraName-value
labels:
myLabelKey1: myLabelValue1
myLabelKey2: myLabelValue2
kubeCertAgent:
namePrefix: kube-cert-agent-name-prefix-
image: kube-cert-agent-image
imagePullSecrets: [kube-cert-agent-image-pull-secret]
log:
level: all
format: json
`),
wantConfig: &Config{
DiscoveryInfo: DiscoveryInfoSpec{
URL: pointer.StringPtr("https://some.discovery/url"),
},
APIConfig: APIConfigSpec{
ServingCertificateConfig: ServingCertificateConfigSpec{
DurationSeconds: pointer.Int64Ptr(3600),
RenewBeforeSeconds: pointer.Int64Ptr(2400),
},
},
APIGroupSuffix: pointer.StringPtr("some.suffix.com"),
AggregatedAPIServerPort: pointer.Int64Ptr(12345),
ImpersonationProxyServerPort: pointer.Int64Ptr(4242),
NamesConfig: NamesConfigSpec{
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
CredentialIssuer: "pinniped-config",
APIService: "pinniped-api",
ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value",
ImpersonationClusterIPService: "impersonationClusterIPService-value",
ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value",
ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value",
ImpersonationSignerSecret: "impersonationSignerSecret-value",
AgentServiceAccount: "agentServiceAccount-value",
},
Labels: map[string]string{
"myLabelKey1": "myLabelValue1",
"myLabelKey2": "myLabelValue2",
},
KubeCertAgentConfig: KubeCertAgentSpec{
NamePrefix: pointer.StringPtr("kube-cert-agent-name-prefix-"),
Image: pointer.StringPtr("kube-cert-agent-image"),
ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"},
},
Log: plog.LogSpec{
Level: plog.LevelAll,
Format: plog.FormatJSON,
},
},
},
{
name: "Fully filled out old log and new log struct",
yaml: here.Doc(`
---
discovery:
url: https://some.discovery/url
api:
servingCertificate:
durationSeconds: 3600
renewBeforeSeconds: 2400
apiGroupSuffix: some.suffix.com
aggregatedAPIServerPort: 12345
impersonationProxyServerPort: 4242
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
apiService: pinniped-api
kubeCertAgentPrefix: kube-cert-agent-prefix
impersonationLoadBalancerService: impersonationLoadBalancerService-value
impersonationClusterIPService: impersonationClusterIPService-value
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
impersonationCACertificateSecret: impersonationCACertificateSecret-value
impersonationSignerSecret: impersonationSignerSecret-value
impersonationSignerSecret: impersonationSignerSecret-value
agentServiceAccount: agentServiceAccount-value
extraName: extraName-value
labels:
myLabelKey1: myLabelValue1
myLabelKey2: myLabelValue2
kubeCertAgent:
namePrefix: kube-cert-agent-name-prefix-
image: kube-cert-agent-image
imagePullSecrets: [kube-cert-agent-image-pull-secret]
logLevel: debug
log:
level: all
format: json
`),
wantConfig: &Config{
DiscoveryInfo: DiscoveryInfoSpec{
URL: pointer.StringPtr("https://some.discovery/url"),
},
APIConfig: APIConfigSpec{
ServingCertificateConfig: ServingCertificateConfigSpec{
DurationSeconds: pointer.Int64Ptr(3600),
RenewBeforeSeconds: pointer.Int64Ptr(2400),
},
},
APIGroupSuffix: pointer.StringPtr("some.suffix.com"),
AggregatedAPIServerPort: pointer.Int64Ptr(12345),
ImpersonationProxyServerPort: pointer.Int64Ptr(4242),
NamesConfig: NamesConfigSpec{
ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate",
CredentialIssuer: "pinniped-config",
APIService: "pinniped-api",
ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value",
ImpersonationClusterIPService: "impersonationClusterIPService-value",
ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value",
ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value",
ImpersonationSignerSecret: "impersonationSignerSecret-value",
AgentServiceAccount: "agentServiceAccount-value",
},
Labels: map[string]string{
"myLabelKey1": "myLabelValue1",
"myLabelKey2": "myLabelValue2",
},
KubeCertAgentConfig: KubeCertAgentSpec{
NamePrefix: pointer.StringPtr("kube-cert-agent-name-prefix-"),
Image: pointer.StringPtr("kube-cert-agent-image"),
ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"},
},
LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelDebug),
Log: plog.LogSpec{
Level: plog.LevelDebug,
Format: plog.FormatJSON,
},
},
},
{
name: "invalid log format",
yaml: here.Doc(`
---
names:
servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate
credentialIssuer: pinniped-config
apiService: pinniped-api
impersonationLoadBalancerService: impersonationLoadBalancerService-value
impersonationClusterIPService: impersonationClusterIPService-value
impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value
impersonationCACertificateSecret: impersonationCACertificateSecret-value
impersonationSignerSecret: impersonationSignerSecret-value
agentServiceAccount: agentServiceAccount-value
log:
level: all
format: snorlax
`),
wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: invalid log format, valid choices are the empty string, json and text",
},
{ {
name: "When only the required fields are present, causes other fields to be defaulted", name: "When only the required fields are present, causes other fields to be defaulted",
yaml: here.Doc(` yaml: here.Doc(`
@ -404,6 +582,8 @@ func TestFromPath(t *testing.T) {
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
// this is a serial test because it sets the global logger
// Write yaml to temp file // Write yaml to temp file
f, err := ioutil.TempFile("", "pinniped-test-config-yaml-*") f, err := ioutil.TempFile("", "pinniped-test-config-yaml-*")
require.NoError(t, err) require.NoError(t, err)
@ -417,7 +597,9 @@ func TestFromPath(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Test FromPath() // Test FromPath()
config, err := FromPath(f.Name()) ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
config, err := FromPath(ctx, f.Name())
if test.wantError != "" { if test.wantError != "" {
require.EqualError(t, err, test.wantError) require.EqualError(t, err, test.wantError)

View File

@ -15,7 +15,9 @@ type Config struct {
NamesConfig NamesConfigSpec `json:"names"` NamesConfig NamesConfigSpec `json:"names"`
KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"` KubeCertAgentConfig KubeCertAgentSpec `json:"kubeCertAgent"`
Labels map[string]string `json:"labels"` Labels map[string]string `json:"labels"`
LogLevel plog.LogLevel `json:"logLevel"` // Deprecated: use log.level instead
LogLevel *plog.LogLevel `json:"logLevel"`
Log plog.LogSpec `json:"log"`
} }
// DiscoveryInfoSpec contains configuration knobs specific to // DiscoveryInfoSpec contains configuration knobs specific to

View File

@ -6,6 +6,7 @@
package supervisor package supervisor
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -28,7 +29,7 @@ const (
// FromPath loads an Config from a provided local file path, inserts any // FromPath loads an Config from a provided local file path, inserts any
// defaults (from the Config documentation), and verifies that the config is // defaults (from the Config documentation), and verifies that the config is
// valid (Config documentation). // valid (Config documentation).
func FromPath(path string) (*Config, error) { func FromPath(ctx context.Context, path string) (*Config, error) {
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("read file: %w", err) return nil, fmt.Errorf("read file: %w", err)
@ -53,7 +54,8 @@ func FromPath(path string) (*Config, error) {
return nil, fmt.Errorf("validate names: %w", err) return nil, fmt.Errorf("validate names: %w", err)
} }
if err := plog.ValidateAndSetLogLevelGlobally(config.LogLevel); err != nil { plog.MaybeSetDeprecatedLogLevel(config.LogLevel, &config.Log)
if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, config.Log); err != nil {
return nil, fmt.Errorf("validate log level: %w", err) return nil, fmt.Errorf("validate log level: %w", err)
} }

View File

@ -4,6 +4,7 @@
package supervisor package supervisor
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -13,6 +14,7 @@ import (
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/plog"
) )
func TestFromPath(t *testing.T) { func TestFromPath(t *testing.T) {
@ -40,6 +42,7 @@ func TestFromPath(t *testing.T) {
network: tcp network: tcp
address: 127.0.0.1:1234 address: 127.0.0.1:1234
insecureAcceptExternalUnencryptedHttpRequests: false insecureAcceptExternalUnencryptedHttpRequests: false
logLevel: trace
`), `),
wantConfig: &Config{ wantConfig: &Config{
APIGroupSuffix: pointer.StringPtr("some.suffix.com"), APIGroupSuffix: pointer.StringPtr("some.suffix.com"),
@ -61,8 +64,122 @@ func TestFromPath(t *testing.T) {
}, },
}, },
AllowExternalHTTP: false, AllowExternalHTTP: false,
LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelTrace),
Log: plog.LogSpec{
Level: plog.LevelTrace,
}, },
}, },
},
{
name: "Happy with new log field",
yaml: here.Doc(`
---
apiGroupSuffix: some.suffix.com
labels:
myLabelKey1: myLabelValue1
myLabelKey2: myLabelValue2
names:
defaultTLSCertificateSecret: my-secret-name
endpoints:
https:
network: unix
address: :1234
http:
network: tcp
address: 127.0.0.1:1234
insecureAcceptExternalUnencryptedHttpRequests: false
log:
level: info
format: text
`),
wantConfig: &Config{
APIGroupSuffix: pointer.StringPtr("some.suffix.com"),
Labels: map[string]string{
"myLabelKey1": "myLabelValue1",
"myLabelKey2": "myLabelValue2",
},
NamesConfig: NamesConfigSpec{
DefaultTLSCertificateSecret: "my-secret-name",
},
Endpoints: &Endpoints{
HTTPS: &Endpoint{
Network: "unix",
Address: ":1234",
},
HTTP: &Endpoint{
Network: "tcp",
Address: "127.0.0.1:1234",
},
},
AllowExternalHTTP: false,
Log: plog.LogSpec{
Level: plog.LevelInfo,
Format: plog.FormatText,
},
},
},
{
name: "Happy with old and new log field",
yaml: here.Doc(`
---
apiGroupSuffix: some.suffix.com
labels:
myLabelKey1: myLabelValue1
myLabelKey2: myLabelValue2
names:
defaultTLSCertificateSecret: my-secret-name
endpoints:
https:
network: unix
address: :1234
http:
network: tcp
address: 127.0.0.1:1234
insecureAcceptExternalUnencryptedHttpRequests: false
logLevel: trace
log:
level: info
format: text
`),
wantConfig: &Config{
APIGroupSuffix: pointer.StringPtr("some.suffix.com"),
Labels: map[string]string{
"myLabelKey1": "myLabelValue1",
"myLabelKey2": "myLabelValue2",
},
NamesConfig: NamesConfigSpec{
DefaultTLSCertificateSecret: "my-secret-name",
},
Endpoints: &Endpoints{
HTTPS: &Endpoint{
Network: "unix",
Address: ":1234",
},
HTTP: &Endpoint{
Network: "tcp",
Address: "127.0.0.1:1234",
},
},
AllowExternalHTTP: false,
LogLevel: func(level plog.LogLevel) *plog.LogLevel { return &level }(plog.LevelTrace),
Log: plog.LogSpec{
Level: plog.LevelTrace,
Format: plog.FormatText,
},
},
},
{
name: "bad log format",
yaml: here.Doc(`
---
names:
defaultTLSCertificateSecret: my-secret-name
log:
level: info
format: cli
`),
wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: invalid log format, valid choices are the empty string, json and text",
},
{ {
name: "When only the required fields are present, causes other fields to be defaulted", name: "When only the required fields are present, causes other fields to be defaulted",
yaml: here.Doc(` yaml: here.Doc(`
@ -307,7 +424,7 @@ func TestFromPath(t *testing.T) {
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
t.Parallel() // this is a serial test because it sets the global logger
// Write yaml to temp file // Write yaml to temp file
f, err := ioutil.TempFile("", "pinniped-test-config-yaml-*") f, err := ioutil.TempFile("", "pinniped-test-config-yaml-*")
@ -322,7 +439,9 @@ func TestFromPath(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Test FromPath() // Test FromPath()
config, err := FromPath(f.Name()) ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
config, err := FromPath(ctx, f.Name())
if test.wantError != "" { if test.wantError != "" {
require.EqualError(t, err, test.wantError) require.EqualError(t, err, test.wantError)

View File

@ -14,7 +14,9 @@ type Config struct {
APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"` APIGroupSuffix *string `json:"apiGroupSuffix,omitempty"`
Labels map[string]string `json:"labels"` Labels map[string]string `json:"labels"`
NamesConfig NamesConfigSpec `json:"names"` NamesConfig NamesConfigSpec `json:"names"`
LogLevel plog.LogLevel `json:"logLevel"` // Deprecated: use log.level instead
LogLevel *plog.LogLevel `json:"logLevel"`
Log plog.LogSpec `json:"log"`
Endpoints *Endpoints `json:"endpoints"` Endpoints *Endpoints `json:"endpoints"`
AllowExternalHTTP stringOrBoolAsBool `json:"insecureAcceptExternalUnencryptedHttpRequests"` AllowExternalHTTP stringOrBoolAsBool `json:"insecureAcceptExternalUnencryptedHttpRequests"`
} }

View File

@ -32,6 +32,8 @@ type certsExpirerController struct {
renewBefore time.Duration renewBefore time.Duration
secretKey string secretKey string
logger plog.Logger
} }
// NewCertsExpirerController returns a controllerlib.Controller that will delete a // NewCertsExpirerController returns a controllerlib.Controller that will delete a
@ -45,10 +47,12 @@ func NewCertsExpirerController(
withInformer pinnipedcontroller.WithInformerOptionFunc, withInformer pinnipedcontroller.WithInformerOptionFunc,
renewBefore time.Duration, renewBefore time.Duration,
secretKey string, secretKey string,
logger plog.Logger,
) controllerlib.Controller { ) controllerlib.Controller {
const name = "certs-expirer-controller"
return controllerlib.New( return controllerlib.New(
controllerlib.Config{ controllerlib.Config{
Name: "certs-expirer-controller", Name: name,
Syncer: &certsExpirerController{ Syncer: &certsExpirerController{
namespace: namespace, namespace: namespace,
certsSecretResourceName: certsSecretResourceName, certsSecretResourceName: certsSecretResourceName,
@ -56,6 +60,7 @@ func NewCertsExpirerController(
secretInformer: secretInformer, secretInformer: secretInformer,
renewBefore: renewBefore, renewBefore: renewBefore,
secretKey: secretKey, secretKey: secretKey,
logger: logger.WithName(name),
}, },
}, },
withInformer( withInformer(
@ -74,7 +79,7 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error {
return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err) return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err)
} }
if notFound { if notFound {
plog.Info("secret does not exist yet or was deleted", c.logger.Info("secret does not exist yet or was deleted",
"controller", ctx.Name, "controller", ctx.Name,
"namespace", c.namespace, "namespace", c.namespace,
"name", c.certsSecretResourceName, "name", c.certsSecretResourceName,
@ -91,7 +96,7 @@ func (c *certsExpirerController) Sync(ctx controllerlib.Context) error {
certAge := time.Since(notBefore) certAge := time.Since(notBefore)
renewDelta := certAge - c.renewBefore renewDelta := certAge - c.renewBefore
plog.Debug("found renew delta", c.logger.Debug("found renew delta",
"controller", ctx.Name, "controller", ctx.Name,
"namespace", c.namespace, "namespace", c.namespace,
"name", c.certsSecretResourceName, "name", c.certsSecretResourceName,

View File

@ -4,12 +4,15 @@
package apicerts package apicerts
import ( import (
"bytes"
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"errors" "errors"
"io"
"strings"
"testing" "testing"
"time" "time"
@ -24,6 +27,7 @@ import (
kubetesting "k8s.io/client-go/testing" kubetesting "k8s.io/client-go/testing"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
) )
@ -101,6 +105,7 @@ func TestExpirerControllerFilters(t *testing.T) {
withInformer.WithInformer, withInformer.WithInformer,
0, // renewBefore, not needed 0, // renewBefore, not needed
"", // not needed "", // not needed
plog.TestLogger(t, io.Discard),
) )
unrelated := corev1.Secret{} unrelated := corev1.Secret{}
@ -125,10 +130,12 @@ func TestExpirerControllerSync(t *testing.T) {
fillSecretData func(*testing.T, map[string][]byte) fillSecretData func(*testing.T, map[string][]byte)
configKubeAPIClient func(*kubernetesfake.Clientset) configKubeAPIClient func(*kubernetesfake.Clientset)
wantDelete bool wantDelete bool
wantLog string
wantError string wantError string
}{ }{
{ {
name: "secret does not exist", name: "secret does not exist",
wantLog: `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"certs-expirer-controller","caller":"apicerts/certs_expirer.go:<line>$apicerts.(*certsExpirerController).Sync","message":"secret does not exist yet or was deleted","controller":"","namespace":"some-namespace","name":"some-resource-name","key":"some-awesome-key","renewBefore":"0s"}`,
wantDelete: false, wantDelete: false,
}, },
{ {
@ -251,6 +258,8 @@ func TestExpirerControllerSync(t *testing.T) {
0, 0,
) )
var log bytes.Buffer
c := NewCertsExpirerController( c := NewCertsExpirerController(
namespace, namespace,
certsSecretResourceName, certsSecretResourceName,
@ -259,6 +268,7 @@ func TestExpirerControllerSync(t *testing.T) {
controllerlib.WithInformer, controllerlib.WithInformer,
test.renewBefore, test.renewBefore,
fakeTestKey, fakeTestKey,
plog.TestLogger(t, &log),
) )
// Must start informers before calling TestRunSynchronously(). // Must start informers before calling TestRunSynchronously().
@ -268,6 +278,9 @@ func TestExpirerControllerSync(t *testing.T) {
err := controllerlib.TestSync(t, c, controllerlib.Context{ err := controllerlib.TestSync(t, c, controllerlib.Context{
Context: ctx, Context: ctx,
}) })
if len(test.wantLog) > 0 {
require.Equal(t, test.wantLog, strings.TrimSpace(log.String()))
}
if test.wantError != "" { if test.wantError != "" {
require.EqualError(t, err, test.wantError) require.EqualError(t, err, test.wantError)
return return

View File

@ -12,11 +12,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
pinnipedcontroller "go.pinniped.dev/internal/controller" pinnipedcontroller "go.pinniped.dev/internal/controller"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/plog"
) )
const ( const (
@ -139,6 +139,6 @@ func (c *certsManagerController) Sync(ctx controllerlib.Context) error {
return fmt.Errorf("could not create secret: %w", err) return fmt.Errorf("could not create secret: %w", err)
} }
klog.Info("certsManagerController Sync successfully created secret") plog.Info("certsManagerController Sync successfully created secret")
return nil return nil
} }

View File

@ -8,11 +8,11 @@ import (
k8serrors "k8s.io/apimachinery/pkg/api/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/klog/v2"
pinnipedcontroller "go.pinniped.dev/internal/controller" pinnipedcontroller "go.pinniped.dev/internal/controller"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/plog"
) )
type certsObserverController struct { type certsObserverController struct {
@ -55,7 +55,7 @@ func (c *certsObserverController) Sync(_ controllerlib.Context) error {
return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err) return fmt.Errorf("failed to get %s/%s secret: %w", c.namespace, c.certsSecretResourceName, err)
} }
if notFound { if notFound {
klog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted") plog.Info("certsObserverController Sync found that the secret does not exist yet or was deleted")
// The secret does not exist yet or was deleted. // The secret does not exist yet or was deleted.
c.dynamicCertProvider.UnsetCertKeyContent() c.dynamicCertProvider.UnsetCertKeyContent()
return nil return nil
@ -66,6 +66,6 @@ func (c *certsObserverController) Sync(_ controllerlib.Context) error {
return fmt.Errorf("failed to set serving cert/key content from secret %s/%s: %w", c.namespace, c.certsSecretResourceName, err) return fmt.Errorf("failed to set serving cert/key content from secret %s/%s: %w", c.namespace, c.certsSecretResourceName, err)
} }
klog.Info("certsObserverController Sync updated certs in the dynamic cert provider") plog.Info("certsObserverController Sync updated certs in the dynamic cert provider")
return nil return nil
} }

View File

@ -6,15 +6,15 @@ package conditionsutil
import ( import (
"sort" "sort"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/equality"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
"go.pinniped.dev/internal/plog"
) )
// Merge merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. // Merge merges conditions into conditionsToUpdate. If returns true if it merged any error conditions.
func Merge(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log logr.Logger) bool { func Merge(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log plog.MinLogger) bool {
hadErrorCondition := false hadErrorCondition := false
for i := range conditions { for i := range conditions {
cond := conditions[i].DeepCopy() cond := conditions[i].DeepCopy()

View File

@ -45,6 +45,7 @@ import (
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/endpointaddr"
"go.pinniped.dev/internal/plog"
) )
const ( const (
@ -132,8 +133,8 @@ func NewImpersonatorConfigController(
impersonationSigningCertProvider: impersonationSigningCertProvider, impersonationSigningCertProvider: impersonationSigningCertProvider,
impersonatorFunc: impersonatorFunc, impersonatorFunc: impersonatorFunc,
tlsServingCertDynamicCertProvider: dynamiccert.NewServingCert("impersonation-proxy-serving-cert"), tlsServingCertDynamicCertProvider: dynamiccert.NewServingCert("impersonation-proxy-serving-cert"),
infoLog: log.V(2), infoLog: log.V(plog.KlogLevelInfo),
debugLog: log.V(4), debugLog: log.V(plog.KlogLevelDebug),
}, },
}, },
withInformer(credentialIssuerInformer, withInformer(credentialIssuerInformer,

View File

@ -43,8 +43,8 @@ import (
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
) )
func TestImpersonatorConfigControllerOptions(t *testing.T) { func TestImpersonatorConfigControllerOptions(t *testing.T) {
@ -63,7 +63,6 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) {
var credIssuerInformerFilter controllerlib.Filter var credIssuerInformerFilter controllerlib.Filter
var servicesInformerFilter controllerlib.Filter var servicesInformerFilter controllerlib.Filter
var secretsInformerFilter controllerlib.Filter var secretsInformerFilter controllerlib.Filter
var testLog *testlogger.Logger
it.Before(func() { it.Before(func() {
r = require.New(t) r = require.New(t)
@ -73,7 +72,6 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) {
credIssuerInformer := pinnipedInformerFactory.Config().V1alpha1().CredentialIssuers() credIssuerInformer := pinnipedInformerFactory.Config().V1alpha1().CredentialIssuers()
servicesInformer := sharedInformerFactory.Core().V1().Services() servicesInformer := sharedInformerFactory.Core().V1().Services()
secretsInformer := sharedInformerFactory.Core().V1().Secrets() secretsInformer := sharedInformerFactory.Core().V1().Secrets()
testLog = testlogger.New(t)
_ = NewImpersonatorConfigController( _ = NewImpersonatorConfigController(
installedInNamespace, installedInNamespace,
@ -94,7 +92,7 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) {
nil, nil,
caSignerName, caSignerName,
nil, nil,
testLog.Logger, plog.Logr(), // nolint: staticcheck // old test with no log assertions
) )
credIssuerInformerFilter = observableWithInformerOption.GetFilterForInformer(credIssuerInformer) credIssuerInformerFilter = observableWithInformerOption.GetFilterForInformer(credIssuerInformer)
servicesInformerFilter = observableWithInformerOption.GetFilterForInformer(servicesInformer) servicesInformerFilter = observableWithInformerOption.GetFilterForInformer(servicesInformer)
@ -292,7 +290,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
var testHTTPServerInterruptCh chan struct{} var testHTTPServerInterruptCh chan struct{}
var queue *testQueue var queue *testQueue
var validClientCert *tls.Certificate var validClientCert *tls.Certificate
var testLog *testlogger.Logger
var impersonatorFunc = func( var impersonatorFunc = func(
port int, port int,
@ -563,7 +560,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
impersonatorFunc, impersonatorFunc,
caSignerName, caSignerName,
signingCertProvider, signingCertProvider,
testLog.Logger, plog.Logr(), // nolint: staticcheck // old test with no log assertions
) )
controllerlib.TestWrap(t, subject, func(syncer controllerlib.Syncer) controllerlib.Syncer { controllerlib.TestWrap(t, subject, func(syncer controllerlib.Syncer) controllerlib.Syncer {
tlsServingCertDynamicCertProvider = syncer.(*impersonatorConfigController).tlsServingCertDynamicCertProvider tlsServingCertDynamicCertProvider = syncer.(*impersonatorConfigController).tlsServingCertDynamicCertProvider
@ -1120,7 +1117,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
signingCASecret = newSigningKeySecret(caSignerName, signingCACertPEM, signingCAKeyPEM) signingCASecret = newSigningKeySecret(caSignerName, signingCACertPEM, signingCAKeyPEM)
validClientCert, err = ca.IssueClientCert("username", nil, time.Hour) validClientCert, err = ca.IssueClientCert("username", nil, time.Hour)
r.NoError(err) r.NoError(err)
testLog = testlogger.New(t)
}) })
it.After(func() { it.After(func() {

View File

@ -28,7 +28,6 @@ import (
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/klog/v2/klogr"
"k8s.io/utils/clock" "k8s.io/utils/clock"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
@ -39,6 +38,7 @@ import (
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/dynamiccert"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/plog"
) )
const ( const (
@ -179,7 +179,7 @@ func NewAgentController(
dynamicCertProvider, dynamicCertProvider,
&clock.RealClock{}, &clock.RealClock{},
cache.NewExpiring(), cache.NewExpiring(),
klogr.New(), plog.Logr(), // nolint: staticcheck // old controller with lots of log statements
) )
} }

View File

@ -4,8 +4,10 @@
package kubecertagent package kubecertagent
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"strings"
"testing" "testing"
"time" "time"
@ -35,8 +37,8 @@ import (
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/test/testlib" "go.pinniped.dev/test/testlib"
) )
@ -339,7 +341,7 @@ func TestAgentController(t *testing.T) {
"could not ensure agent deployment: some creation error", "could not ensure agent deployment: some creation error",
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
wantStrategy: &configv1alpha1.CredentialIssuerStrategy{ wantStrategy: &configv1alpha1.CredentialIssuerStrategy{
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
@ -386,7 +388,7 @@ func TestAgentController(t *testing.T) {
`could not ensure agent deployment: deployments.apps "pinniped-concierge-kube-cert-agent" already exists`, `could not ensure agent deployment: deployments.apps "pinniped-concierge-kube-cert-agent" already exists`,
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
wantAgentDeployment: healthyAgentDeployment, wantAgentDeployment: healthyAgentDeployment,
wantDeploymentActionVerbs: []string{"list", "watch", "create"}, wantDeploymentActionVerbs: []string{"list", "watch", "create"},
@ -435,7 +437,7 @@ func TestAgentController(t *testing.T) {
`could not ensure agent deployment: deployments.apps "pinniped-concierge-kube-cert-agent" already exists`, `could not ensure agent deployment: deployments.apps "pinniped-concierge-kube-cert-agent" already exists`,
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
wantAgentDeployment: healthyAgentDeploymentWithDefaultedPaths, wantAgentDeployment: healthyAgentDeploymentWithDefaultedPaths,
wantDeploymentActionVerbs: []string{"list", "watch", "create"}, wantDeploymentActionVerbs: []string{"list", "watch", "create"},
@ -461,8 +463,8 @@ func TestAgentController(t *testing.T) {
"could not find a healthy agent pod (1 candidate)", "could not find a healthy agent pod (1 candidate)",
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
wantAgentDeployment: healthyAgentDeployment, wantAgentDeployment: healthyAgentDeployment,
wantDeploymentActionVerbs: []string{"list", "watch", "delete", "create"}, // must recreate deployment when Selector field changes wantDeploymentActionVerbs: []string{"list", "watch", "delete", "create"}, // must recreate deployment when Selector field changes
@ -496,7 +498,7 @@ func TestAgentController(t *testing.T) {
"could not ensure agent deployment: some delete error", "could not ensure agent deployment: some delete error",
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
wantAgentDeployment: healthyAgentDeploymentWithOldStyleSelector, // couldn't be deleted, so it didn't change wantAgentDeployment: healthyAgentDeploymentWithOldStyleSelector, // couldn't be deleted, so it didn't change
// delete to try to recreate deployment when Selector field changes, but delete always fails, so keeps trying to delete // delete to try to recreate deployment when Selector field changes, but delete always fails, so keeps trying to delete
@ -532,9 +534,9 @@ func TestAgentController(t *testing.T) {
"could not ensure agent deployment: some create error", "could not ensure agent deployment: some create error",
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
`kube-cert-agent-controller "level"=0 "msg"="creating new deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"creating new deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
wantAgentDeployment: nil, // was deleted, but couldn't be recreated wantAgentDeployment: nil, // was deleted, but couldn't be recreated
// delete to try to recreate deployment when Selector field changes, but create always fails, so keeps trying to recreate // delete to try to recreate deployment when Selector field changes, but create always fails, so keeps trying to recreate
@ -584,7 +586,7 @@ func TestAgentController(t *testing.T) {
"could not find a healthy agent pod (1 candidate)", "could not find a healthy agent pod (1 candidate)",
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="updating existing deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"updating existing deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
wantAgentDeployment: healthyAgentDeploymentWithExtraLabels, wantAgentDeployment: healthyAgentDeploymentWithExtraLabels,
wantDeploymentActionVerbs: []string{"list", "watch", "update"}, wantDeploymentActionVerbs: []string{"list", "watch", "update"},
@ -619,7 +621,7 @@ func TestAgentController(t *testing.T) {
LastUpdateTime: metav1.NewTime(now), LastUpdateTime: metav1.NewTime(now),
}, },
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="updating existing deployment" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"updating existing deployment","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
}, },
}, },
{ {
@ -931,8 +933,8 @@ func TestAgentController(t *testing.T) {
// delete to try to recreate deployment when Selector field changes, but delete always fails, so keeps trying to delete // delete to try to recreate deployment when Selector field changes, but delete always fails, so keeps trying to delete
wantDeploymentActionVerbs: []string{"list", "watch", "delete", "delete"}, wantDeploymentActionVerbs: []string{"list", "watch", "delete", "delete"},
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="deleting deployment to update immutable Selector field" "deployment"={"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"} "templatePod"={"name":"kube-controller-manager-1","namespace":"kube-system"}`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).createOrUpdateDeployment","message":"deleting deployment to update immutable Selector field","deployment":{"name":"pinniped-concierge-kube-cert-agent","namespace":"concierge"},"templatePod":{"name":"kube-controller-manager-1","namespace":"kube-system"}}`,
`kube-cert-agent-controller "level"=0 "msg"="successfully loaded signing key from agent pod into cache"`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`,
}, },
wantDeploymentDeleteActionOpts: []metav1.DeleteOptions{ wantDeploymentDeleteActionOpts: []metav1.DeleteOptions{
testutil.NewPreconditions(healthyAgentDeploymentWithOldStyleSelector.UID, healthyAgentDeploymentWithOldStyleSelector.ResourceVersion), testutil.NewPreconditions(healthyAgentDeploymentWithOldStyleSelector.UID, healthyAgentDeploymentWithOldStyleSelector.ResourceVersion),
@ -962,7 +964,7 @@ func TestAgentController(t *testing.T) {
wantAgentDeployment: healthyAgentDeployment, wantAgentDeployment: healthyAgentDeployment,
wantDeploymentActionVerbs: []string{"list", "watch"}, wantDeploymentActionVerbs: []string{"list", "watch"},
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="successfully loaded signing key from agent pod into cache"`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`,
}, },
wantStrategy: &configv1alpha1.CredentialIssuerStrategy{ wantStrategy: &configv1alpha1.CredentialIssuerStrategy{
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
@ -996,7 +998,7 @@ func TestAgentController(t *testing.T) {
wantAgentDeployment: healthyAgentDeployment, wantAgentDeployment: healthyAgentDeployment,
wantDeploymentActionVerbs: []string{"list", "watch"}, wantDeploymentActionVerbs: []string{"list", "watch"},
wantDistinctLogs: []string{ wantDistinctLogs: []string{
`kube-cert-agent-controller "level"=0 "msg"="successfully loaded signing key from agent pod into cache"`, `{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"kube-cert-agent-controller","caller":"kubecertagent/kubecertagent.go:<line>$kubecertagent.(*agentController).loadSigningKey","message":"successfully loaded signing key from agent pod into cache"}`,
}, },
wantStrategy: &configv1alpha1.CredentialIssuerStrategy{ wantStrategy: &configv1alpha1.CredentialIssuerStrategy{
Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, Type: configv1alpha1.KubeClusterSigningCertificateStrategyType,
@ -1028,7 +1030,9 @@ func TestAgentController(t *testing.T) {
} }
kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0) kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0)
log := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements
var buf bytes.Buffer
log := plog.TestZapr(t, &buf)
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
defer ctrl.Finish() defer ctrl.Finish()
@ -1066,7 +1070,7 @@ func TestAgentController(t *testing.T) {
mockDynamicCert, mockDynamicCert,
fakeClock, fakeClock,
execCache, execCache,
log.Logger, log,
) )
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@ -1081,7 +1085,7 @@ func TestAgentController(t *testing.T) {
allAllowedErrors = append(allAllowedErrors, tt.alsoAllowUndesiredDistinctErrors...) allAllowedErrors = append(allAllowedErrors, tt.alsoAllowUndesiredDistinctErrors...)
assert.Subsetf(t, allAllowedErrors, actualErrors, "actual errors contained additional error(s) which is not expected by the test") assert.Subsetf(t, allAllowedErrors, actualErrors, "actual errors contained additional error(s) which is not expected by the test")
assert.Equal(t, tt.wantDistinctLogs, deduplicate(log.Lines()), "unexpected logs") assert.Equal(t, tt.wantDistinctLogs, deduplicate(logLines(buf.String())), "unexpected logs")
// Assert on all actions that happened to deployments. // Assert on all actions that happened to deployments.
var actualDeploymentActionVerbs []string var actualDeploymentActionVerbs []string
@ -1124,6 +1128,14 @@ func TestAgentController(t *testing.T) {
} }
} }
func logLines(logs string) []string {
if len(logs) == 0 {
return nil
}
return strings.Split(strings.TrimSpace(logs), "\n")
}
func TestMergeLabelsAndAnnotations(t *testing.T) { func TestMergeLabelsAndAnnotations(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -13,12 +13,10 @@ import (
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/google/uuid" "github.com/google/uuid"
"k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/klog/v2/klogr"
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
@ -28,6 +26,7 @@ import (
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/upstreamldap" "go.pinniped.dev/internal/upstreamldap"
) )
@ -360,7 +359,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context,
} }
func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider, conditions []*v1alpha1.Condition) { func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider, conditions []*v1alpha1.Condition) {
log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name)
updated := upstream.DeepCopy() updated := upstream.DeepCopy()
hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log)
@ -379,7 +378,7 @@ func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, ups
ActiveDirectoryIdentityProviders(upstream.Namespace). ActiveDirectoryIdentityProviders(upstream.Namespace).
UpdateStatus(ctx, updated, metav1.UpdateOptions{}) UpdateStatus(ctx, updated, metav1.UpdateOptions{})
if err != nil { if err != nil {
log.Error(err, "failed to update status") log.Error("failed to update status", err)
} }
} }

View File

@ -12,7 +12,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/klog/v2/klogr"
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
@ -22,6 +21,7 @@ import (
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/upstreamldap" "go.pinniped.dev/internal/upstreamldap"
) )
@ -252,7 +252,7 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *
} }
func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) { func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) {
log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name)
updated := upstream.DeepCopy() updated := upstream.DeepCopy()
hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log)
@ -271,6 +271,6 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al
LDAPIdentityProviders(upstream.Namespace). LDAPIdentityProviders(upstream.Namespace).
UpdateStatus(ctx, updated, metav1.UpdateOptions{}) UpdateStatus(ctx, updated, metav1.UpdateOptions{})
if err != nil { if err != nil {
log.Error(err, "failed to update status") log.Error("failed to update status", err)
} }
} }

View File

@ -14,8 +14,6 @@ import (
"strings" "strings"
"time" "time"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -24,6 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/cache" "k8s.io/apimachinery/pkg/util/cache"
"k8s.io/apimachinery/pkg/util/sets"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
@ -36,6 +35,7 @@ import (
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/internal/upstreamoidc"
) )
@ -67,7 +67,7 @@ const (
) )
var ( var (
disallowedAdditionalAuthorizeParameters = map[string]bool{ //nolint: gochecknoglobals disallowedAdditionalAuthorizeParameters = map[string]bool{ // nolint: gochecknoglobals
// Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters // Reject these AdditionalAuthorizeParameters to avoid allowing the user's config to overwrite the parameters
// that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise // that are always used by Pinniped in authcode authorization requests. The OIDC library used would otherwise
// happily treat the user's config as an override. Users can already set the "client_id" and "scope" params // happily treat the user's config as an override. Users can already set the "client_id" and "scope" params
@ -331,8 +331,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer) discoveredProvider, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), upstream.Spec.Issuer)
if err != nil { if err != nil {
const klogLevelTrace = 6 c.log.V(plog.KlogLevelTrace).WithValues(
c.log.V(klogLevelTrace).WithValues(
"namespace", upstream.Namespace, "namespace", upstream.Namespace,
"name", upstream.Name, "name", upstream.Name,
"issuer", upstream.Spec.Issuer, "issuer", upstream.Spec.Issuer,

View File

@ -29,6 +29,7 @@ import (
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/internal/testutil/oidctestutil"
"go.pinniped.dev/internal/testutil/testlogger" "go.pinniped.dev/internal/testutil/testlogger"
@ -78,7 +79,6 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) {
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0)
fakeKubeClient := fake.NewSimpleClientset() fakeKubeClient := fake.NewSimpleClientset()
kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0)
testLog := testlogger.New(t)
cache := provider.NewDynamicUpstreamIDPProvider() cache := provider.NewDynamicUpstreamIDPProvider()
cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{
&upstreamoidc.ProviderConfig{Name: "initial-entry"}, &upstreamoidc.ProviderConfig{Name: "initial-entry"},
@ -91,7 +91,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) {
nil, nil,
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
secretInformer, secretInformer,
testLog.Logger, plog.Logr(), // nolint: staticcheck // old test with no log assertions
withInformer.WithInformer, withInformer.WithInformer,
) )
@ -1400,7 +1400,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0)
fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...)
kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0)
testLog := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements testLog := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements
cache := provider.NewDynamicUpstreamIDPProvider() cache := provider.NewDynamicUpstreamIDPProvider()
cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{
&upstreamoidc.ProviderConfig{Name: "initial-entry"}, &upstreamoidc.ProviderConfig{Name: "initial-entry"},

View File

@ -15,7 +15,6 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/events" "k8s.io/client-go/tools/events"
"k8s.io/client-go/util/workqueue" "k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
) )
@ -215,7 +214,7 @@ func (c *controller) handleKey(key Key, err error) {
if errors.Is(err, ErrSyntheticRequeue) { if errors.Is(err, ErrSyntheticRequeue) {
// logging this helps detecting wedged controllers with missing pre-requirements // logging this helps detecting wedged controllers with missing pre-requirements
klog.V(4).InfoS("requested synthetic requeue", "controller", c.Name(), "key", key) plog.Debug("requested synthetic requeue", "controller", c.Name(), "key", key)
} else { } else {
utilruntime.HandleError(fmt.Errorf("%s: %v failed with: %w", c.Name(), key, err)) utilruntime.HandleError(fmt.Errorf("%s: %v failed with: %w", c.Name(), key, err))
} }

View File

@ -13,7 +13,8 @@ import (
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/events" "k8s.io/client-go/tools/events"
"k8s.io/client-go/util/workqueue" "k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/plog"
) )
type Option func(*controller) type Option func(*controller)
@ -62,7 +63,7 @@ func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Opti
AddFunc: func(obj interface{}) { AddFunc: func(obj interface{}) {
object := metaOrDie(obj) object := metaOrDie(obj)
if filter.Add(object) { if filter.Add(object) {
klog.V(4).InfoS("handling add", plog.Debug("handling add",
"controller", c.Name(), "controller", c.Name(),
"namespace", object.GetNamespace(), "namespace", object.GetNamespace(),
"name", object.GetName(), "name", object.GetName(),
@ -76,7 +77,7 @@ func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Opti
oldObject := metaOrDie(oldObj) oldObject := metaOrDie(oldObj)
newObject := metaOrDie(newObj) newObject := metaOrDie(newObj)
if filter.Update(oldObject, newObject) { if filter.Update(oldObject, newObject) {
klog.V(4).InfoS("handling update", plog.Debug("handling update",
"controller", c.Name(), "controller", c.Name(),
"namespace", newObject.GetNamespace(), "namespace", newObject.GetNamespace(),
"name", newObject.GetName(), "name", newObject.GetName(),
@ -101,7 +102,7 @@ func WithInformer(getter InformerGetter, filter Filter, opt InformerOption) Opti
} }
} }
if filter.Delete(accessor) { if filter.Delete(accessor) {
klog.V(4).InfoS("handling delete", plog.Debug("handling delete",
"controller", c.Name(), "controller", c.Name(),
"namespace", accessor.GetNamespace(), "namespace", accessor.GetNamespace(),
"name", accessor.GetName(), "name", accessor.GetName(),

View File

@ -8,7 +8,8 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/events" "k8s.io/client-go/tools/events"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/plog"
) )
var _ events.EventRecorder = klogRecorder{} var _ events.EventRecorder = klogRecorder{}
@ -16,7 +17,7 @@ var _ events.EventRecorder = klogRecorder{}
type klogRecorder struct{} type klogRecorder struct{}
func (n klogRecorder) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) { func (n klogRecorder) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) {
klog.V(4).InfoS("recording event", plog.Debug("recording event",
"regarding", regarding, "regarding", regarding,
"related", related, "related", related,
"eventtype", eventtype, "eventtype", eventtype,

View File

@ -14,10 +14,10 @@ import (
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/events" "k8s.io/client-go/tools/events"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api" "go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api"
"go.pinniped.dev/internal/plog"
) )
//nolint:funlen //nolint:funlen
@ -59,7 +59,7 @@ func NewExampleCreatingController(
} }
generateSecret := func(service *corev1.Service) error { generateSecret := func(service *corev1.Service) error {
klog.V(4).InfoS("generating new secret for service", "namespace", service.Namespace, "name", service.Name) plog.Debug("generating new secret for service", "namespace", service.Namespace, "name", service.Name)
secret := &corev1.Secret{ secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -97,7 +97,7 @@ func NewExampleCreatingController(
return nil // drop from queue because we cannot safely update this secret return nil // drop from queue because we cannot safely update this secret
} }
klog.V(4).InfoS("updating data in existing secret", "namespace", secret.Namespace, "name", secret.Name) plog.Debug("updating data in existing secret", "namespace", secret.Namespace, "name", secret.Name)
// Actually update the secret in the regeneration case (the secret already exists but we want to update to new secretData). // Actually update the secret in the regeneration case (the secret already exists but we want to update to new secretData).
_, updateErr := secretClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) _, updateErr := secretClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
return updateErr return updateErr
@ -169,7 +169,7 @@ func NewExampleCreatingController(
utilruntime.HandleError(fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err)) utilruntime.HandleError(fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err))
return false return false
} }
klog.V(4).InfoS("recreating secret", "namespace", service.Namespace, "name", service.Name) plog.Debug("recreating secret", "namespace", service.Namespace, "name", service.Name)
return true return true
}, },
}, controllerlib.InformerOption{}), }, controllerlib.InformerOption{}),

View File

@ -11,7 +11,6 @@ import (
k8sinformers "k8s.io/client-go/informers" k8sinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/klog/v2/klogr"
"k8s.io/utils/clock" "k8s.io/utils/clock"
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
@ -34,6 +33,7 @@ import (
"go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/groupsuffix"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/leaderelection" "go.pinniped.dev/internal/leaderelection"
"go.pinniped.dev/internal/plog"
) )
const ( const (
@ -197,6 +197,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) {
controllerlib.WithInformer, controllerlib.WithInformer,
c.ServingCertRenewBefore, c.ServingCertRenewBefore,
apicerts.TLSCertificateChainSecretKey, apicerts.TLSCertificateChainSecretKey,
plog.New(),
), ),
singletonWorker, singletonWorker,
). ).
@ -222,7 +223,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) {
agentConfig, agentConfig,
client, client,
informers.installationNamespaceK8s.Core().V1().Pods(), informers.installationNamespaceK8s.Core().V1().Pods(),
klogr.New(), plog.Logr(), // nolint: staticcheck // old controller with lots of log statements
), ),
singletonWorker, singletonWorker,
). ).
@ -232,7 +233,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) {
webhookcachefiller.New( webhookcachefiller.New(
c.AuthenticatorCache, c.AuthenticatorCache,
informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(),
klogr.New(), plog.Logr(), // nolint: staticcheck // old controller with lots of log statements
), ),
singletonWorker, singletonWorker,
). ).
@ -240,7 +241,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) {
jwtcachefiller.New( jwtcachefiller.New(
c.AuthenticatorCache, c.AuthenticatorCache,
informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(), informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(),
klogr.New(), plog.Logr(), // nolint: staticcheck // old controller with lots of log statements
), ),
singletonWorker, singletonWorker,
). ).
@ -249,7 +250,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) {
c.AuthenticatorCache, c.AuthenticatorCache,
informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(), informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(),
informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(), informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(),
klogr.New(), plog.Logr(), // nolint: staticcheck // old controller with lots of log statements
), ),
singletonWorker, singletonWorker,
). ).
@ -275,7 +276,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) {
impersonator.New, impersonator.New,
c.NamesConfig.ImpersonationSignerSecret, c.NamesConfig.ImpersonationSignerSecret,
c.ImpersonationSigningCertProvider, c.ImpersonationSigningCertProvider,
klogr.New(), plog.Logr(), // nolint: staticcheck // old controller with lots of log statements
), ),
singletonWorker, singletonWorker,
). ).
@ -303,6 +304,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) {
controllerlib.WithInformer, controllerlib.WithInformer,
365*24*time.Hour-time.Hour, // 1 year minus 1 hour hard coded value (i.e. wait until the last moment to break the signer) 365*24*time.Hour-time.Hour, // 1 year minus 1 hour hard coded value (i.e. wait until the last moment to break the signer)
apicerts.CACertificateSecretKey, apicerts.CACertificateSecretKey,
plog.New(),
), ),
singletonWorker, singletonWorker,
) )

View File

@ -20,7 +20,8 @@ import (
_ "crypto/tls/fipsonly" // restricts all TLS configuration to FIPS-approved settings. _ "crypto/tls/fipsonly" // restricts all TLS configuration to FIPS-approved settings.
"k8s.io/apiserver/pkg/server/options" "k8s.io/apiserver/pkg/server/options"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/plog"
) )
// Always use TLS 1.2 for FIPs // Always use TLS 1.2 for FIPs
@ -36,9 +37,7 @@ func init() {
// this init runs before we have parsed our config to determine our log level // this init runs before we have parsed our config to determine our log level
// thus we must use a log statement that will always print instead of conditionally print // thus we must use a log statement that will always print instead of conditionally print
// for plog, that is only error and warning logs, neither of which seem appropriate here plog.Always("using boring crypto in fips only mode", "go version", runtime.Version())
// therefore, just use klog directly with no V level requirement
klog.InfoS("using boring crypto in fips only mode", "go version", runtime.Version())
} }
func Default(rootCAs *x509.CertPool) *tls.Config { func Default(rootCAs *x509.CertPool) *tls.Config {

View File

@ -16,7 +16,6 @@ import (
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/plog"
) )
const ( const (
@ -86,8 +85,8 @@ func New(apiGroupSuffix string) kubeclient.Middleware {
// want our middleware to be opinionated so that it can be really good at a specific task // want our middleware to be opinionated so that it can be really good at a specific task
// and give us specific feedback when it can't do that specific task // and give us specific feedback when it can't do that specific task
return fmt.Errorf( return fmt.Errorf(
"cannot replace token credential request %q without authenticator API group", "cannot replace token credential request %s/%s without authenticator API group",
plog.KObj(obj), obj.GetNamespace(), obj.GetName(),
) )
} }
@ -95,8 +94,8 @@ func New(apiGroupSuffix string) kubeclient.Middleware {
if !ok { if !ok {
// see comment above about specificity of middleware // see comment above about specificity of middleware
return fmt.Errorf( return fmt.Errorf(
"cannot replace token credential request %q authenticator API group %q with group suffix %q", "cannot replace token credential request %s/%s authenticator API group %q with group suffix %q",
plog.KObj(obj), obj.GetNamespace(), obj.GetName(),
*tokenCredentialRequest.Spec.Authenticator.APIGroup, *tokenCredentialRequest.Spec.Authenticator.APIGroup,
apiGroupSuffix, apiGroupSuffix,
) )
@ -176,7 +175,7 @@ func Unreplace(baseAPIGroup, apiGroupSuffix string) (string, bool) {
// makes sure that the provided apiGroupSuffix is a valid DNS-1123 subdomain with at least one dot, // makes sure that the provided apiGroupSuffix is a valid DNS-1123 subdomain with at least one dot,
// to match Kubernetes behavior. // to match Kubernetes behavior.
func Validate(apiGroupSuffix string) error { func Validate(apiGroupSuffix string) error {
var errs []error //nolint: prealloc var errs []error // nolint: prealloc
if len(strings.Split(apiGroupSuffix, ".")) < 2 { if len(strings.Split(apiGroupSuffix, ".")) < 2 {
errs = append(errs, constable.Error("must contain '.'")) errs = append(errs, constable.Error("must contain '.'"))

View File

@ -435,7 +435,7 @@ func TestMiddlware(t *testing.T) {
responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec
wantMutateRequests: 3, wantMutateRequests: 3,
wantMutateResponses: 1, wantMutateResponses: 1,
wantMutateRequestErrors: []string{`cannot replace token credential request "/" authenticator API group "authentication.concierge.some.suffix.com" with group suffix "some.suffix.com"`}, wantMutateRequestErrors: []string{`cannot replace token credential request / authenticator API group "authentication.concierge.some.suffix.com" with group suffix "some.suffix.com"`},
wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
}, },
{ {
@ -475,7 +475,7 @@ func TestMiddlware(t *testing.T) {
responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec
wantMutateRequests: 3, wantMutateRequests: 3,
wantMutateResponses: 1, wantMutateResponses: 1,
wantMutateRequestErrors: []string{`cannot replace token credential request "/" without authenticator API group`}, wantMutateRequestErrors: []string{`cannot replace token credential request / without authenticator API group`},
wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
}, },
{ {

View File

@ -39,9 +39,9 @@ func glogBody(prefix string, body []byte) {
if bytes.IndexFunc(body, func(r rune) bool { if bytes.IndexFunc(body, func(r rune) bool {
return r < 0x0a return r < 0x0a
}) != -1 { }) != -1 {
plog.Debug(prefix, "body", hex.Dump(body)) plog.All(prefix, "body", hex.Dump(body))
} else { } else {
plog.Debug(prefix, "body", string(body)) plog.All(prefix, "body", string(body))
} }
} }
} }

View File

@ -22,6 +22,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"strings" "strings"
"syscall"
"time" "time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -31,7 +32,6 @@ import (
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
corev1informers "k8s.io/client-go/informers/core/v1" corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/controller/apicerts" "go.pinniped.dev/internal/controller/apicerts"
@ -339,10 +339,7 @@ func waitForSignal() os.Signal {
return <-signalCh return <-signalCh
} }
func run() error { func run(ctx context.Context) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client, err := kubeclient.New() client, err := kubeclient.New()
if err != nil { if err != nil {
return fmt.Errorf("cannot create k8s client: %w", err) return fmt.Errorf("cannot create k8s client: %w", err)
@ -359,7 +356,7 @@ func run() error {
startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers) startControllers(ctx, dynamicCertProvider, client.Kubernetes, kubeInformers)
plog.Debug("controllers are ready") plog.Debug("controllers are ready")
//nolint: gosec // Intentionally binding to all network interfaces. // nolint: gosec // Intentionally binding to all network interfaces.
l, err := net.Listen("tcp", ":8443") l, err := net.Listen("tcp", ":8443")
if err != nil { if err != nil {
return fmt.Errorf("cannot create listener: %w", err) return fmt.Errorf("cannot create listener: %w", err)
@ -378,13 +375,35 @@ func run() error {
return nil return nil
} }
func Main() { func main() error { // return an error instead of plog.Fatal to allow defer statements to run
ctx := signalCtx()
// Hardcode the logging level to debug, since this is a test app and it is very helpful to have // Hardcode the logging level to debug, since this is a test app and it is very helpful to have
// verbose logs to debug test failures. // verbose logs to debug test failures.
if err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug); err != nil { if err := plog.ValidateAndSetLogLevelAndFormatGlobally(ctx, plog.LogSpec{Level: plog.LevelDebug}); err != nil {
klog.Fatal(err) plog.Fatal(err)
} }
if err := run(); err != nil {
klog.Fatal(err) return run(ctx)
}
func Main() {
if err := main(); err != nil {
plog.Fatal(err)
} }
} }
func signalCtx() context.Context {
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
go func() {
defer cancel()
s := <-signalCh
plog.Debug("saw signal", "signal", s)
}()
return ctx
}

View File

@ -7,23 +7,22 @@ package auth
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"time" "time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc" coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/felixge/httpsnoop"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
"github.com/pkg/errors"
"golang.org/x/oauth2" "golang.org/x/oauth2"
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc" supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/httputil/securityheader"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/csrftoken"
"go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/downstreamsession"
"go.pinniped.dev/internal/oidc/login"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/psession"
@ -55,6 +54,12 @@ func NewHandler(
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method) return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
} }
// Note that the client might have used supervisoroidc.AuthorizeUpstreamIDPNameParamName and
// supervisoroidc.AuthorizeUpstreamIDPTypeParamName query params to request a certain upstream IDP.
// The Pinniped CLI has been sending these params since v0.9.0.
// Currently, these are ignored because the Supervisor does not yet support logins when multiple IDPs
// are configured. However, these params should be honored in the future when choosing an upstream
// here, e.g. by calling supervisoroidc.FindUpstreamIDPByNameAndType() when the params are present.
oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister) oidcUpstream, ldapUpstream, idpType, err := chooseUpstreamIDP(idpLister)
if err != nil { if err != nil {
plog.WarningErr("authorize upstream config", err) plog.WarningErr("authorize upstream config", err)
@ -62,11 +67,12 @@ func NewHandler(
} }
if idpType == psession.ProviderTypeOIDC { if idpType == psession.ProviderTypeOIDC {
if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 { if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 ||
len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 {
// The client set a username header, so they are trying to log in with a username/password. // The client set a username header, so they are trying to log in with a username/password.
return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream) return handleAuthRequestForOIDCUpstreamPasswordGrant(r, w, oauthHelperWithStorage, oidcUpstream)
} }
return handleAuthRequestForOIDCUpstreamAuthcodeGrant(r, w, return handleAuthRequestForOIDCUpstreamBrowserFlow(r, w,
oauthHelperWithoutStorage, oauthHelperWithoutStorage,
generateCSRF, generateNonce, generatePKCE, generateCSRF, generateNonce, generatePKCE,
oidcUpstream, oidcUpstream,
@ -75,15 +81,34 @@ func NewHandler(
cookieCodec, cookieCodec,
) )
} }
return handleAuthRequestForLDAPUpstream(r, w,
// We know it's an AD/LDAP upstream.
if len(r.Header.Values(supervisoroidc.AuthorizeUsernameHeaderName)) > 0 ||
len(r.Header.Values(supervisoroidc.AuthorizePasswordHeaderName)) > 0 {
// The client set a username header, so they are trying to log in with a username/password.
return handleAuthRequestForLDAPUpstreamCLIFlow(r, w,
oauthHelperWithStorage, oauthHelperWithStorage,
ldapUpstream, ldapUpstream,
idpType, idpType,
) )
}
return handleAuthRequestForLDAPUpstreamBrowserFlow(
r,
w,
oauthHelperWithoutStorage,
generateCSRF,
generateNonce,
generatePKCE,
ldapUpstream,
idpType,
downstreamIssuer,
upstreamStateEncoder,
cookieCodec,
)
})) }))
} }
func handleAuthRequestForLDAPUpstream( func handleAuthRequestForLDAPUpstreamCLIFlow(
r *http.Request, r *http.Request,
w http.ResponseWriter, w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
@ -106,36 +131,55 @@ func handleAuthRequestForLDAPUpstream(
return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication") return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication")
} }
if !authenticated { if !authenticated {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."), true) fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."), true)
return nil
} }
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
username = authenticateResponse.User.GetName() username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
dn := authenticateResponse.DN customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
customSessionData := &psession.CustomSessionData{ return nil
ProviderUID: ldapUpstream.GetResourceUID(), }
ProviderName: ldapUpstream.GetName(),
ProviderType: idpType, func handleAuthRequestForLDAPUpstreamBrowserFlow(
r *http.Request,
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
generateCSRF func() (csrftoken.CSRFToken, error),
generateNonce func() (nonce.Nonce, error),
generatePKCE func() (pkce.Code, error),
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
idpType psession.ProviderType,
downstreamIssuer string,
upstreamStateEncoder oidc.Encoder,
cookieCodec oidc.Codec,
) error {
authRequestState, err := handleBrowserFlowAuthRequest(
r,
w,
oauthHelper,
generateCSRF,
generateNonce,
generatePKCE,
ldapUpstream.GetName(),
idpType,
cookieCodec,
upstreamStateEncoder,
)
if err != nil {
return err
}
if authRequestState == nil {
// There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it.
return nil
} }
if idpType == psession.ProviderTypeLDAP { return login.RedirectToLoginPage(r, w, downstreamIssuer, authRequestState.encodedStateParam, login.ShowNoError)
customSessionData.LDAP = &psession.LDAPSessionData{
UserDN: dn,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
if idpType == psession.ProviderTypeActiveDirectory {
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
UserDN: dn,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
} }
func handleAuthRequestForOIDCUpstreamPasswordGrant( func handleAuthRequestForOIDCUpstreamPasswordGrant(
@ -156,9 +200,10 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
if !oidcUpstream.AllowsPasswordGrant() { if !oidcUpstream.AllowsPasswordGrant() {
// Return a user-friendly error for this case which is entirely within our control. // Return a user-friendly error for this case which is entirely within our control.
return writeAuthorizeError(w, oauthHelper, authorizeRequester, oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHint( fosite.ErrAccessDenied.WithHint(
"Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), true) "Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), true)
return nil
} }
token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password) token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password)
@ -170,29 +215,36 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
// However, the exact response is undefined in the sense that there is no such thing as a password grant in // However, the exact response is undefined in the sense that there is no such thing as a password grant in
// the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the // the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the
// spec and returns something other than an "invalid_grant" error for bad resource owner credentials.) // spec and returns something other than an "invalid_grant" error for bad resource owner credentials.)
return writeAuthorizeError(w, oauthHelper, authorizeRequester, oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithDebug(err.Error()), true) // WithDebug hides the error from the client fosite.ErrAccessDenied.WithDebug(err.Error()), true) // WithDebug hides the error from the client
return nil
} }
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims)
if err != nil { if err != nil {
// Return a user-friendly error for this case which is entirely within our control. // Return a user-friendly error for this case which is entirely within our control.
return writeAuthorizeError(w, oauthHelper, authorizeRequester, oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
) )
return nil
} }
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token) customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token)
if err != nil { if err != nil {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
) )
return nil
} }
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups, customSessionData) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, true)
return nil
} }
func handleAuthRequestForOIDCUpstreamAuthcodeGrant( func handleAuthRequestForOIDCUpstreamBrowserFlow(
r *http.Request, r *http.Request,
w http.ResponseWriter, w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
@ -204,34 +256,24 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
upstreamStateEncoder oidc.Encoder, upstreamStateEncoder oidc.Encoder,
cookieCodec oidc.Codec, cookieCodec oidc.Codec,
) error { ) error {
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false) authRequestState, err := handleBrowserFlowAuthRequest(
if !created { r,
return nil w,
} oauthHelper,
generateCSRF,
now := time.Now() generateNonce,
_, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ generatePKCE,
Fosite: &openid.DefaultSession{ oidcUpstream.GetName(),
Claims: &jwt.IDTokenClaims{ psession.ProviderTypeOIDC,
// Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. cookieCodec,
Subject: "none", upstreamStateEncoder,
AuthTime: now, )
RequestedAt: now,
},
},
})
if err != nil { if err != nil {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false)
}
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
if err != nil {
plog.Error("authorize generate error", err)
return err return err
} }
csrfFromCookie := readCSRFCookie(r, cookieCodec) if authRequestState == nil {
if csrfFromCookie != "" { // There was an error but handleBrowserFlowAuthRequest() already took care of writing the response for it.
csrfValue = csrfFromCookie return nil
} }
upstreamOAuthConfig := oauth2.Config{ upstreamOAuthConfig := oauth2.Config{
@ -243,46 +285,19 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
Scopes: oidcUpstream.GetScopes(), Scopes: oidcUpstream.GetScopes(),
} }
encodedStateParamValue, err := upstreamStateParam(
authorizeRequester,
oidcUpstream.GetName(),
nonceValue,
csrfValue,
pkceValue,
upstreamStateEncoder,
)
if err != nil {
plog.Error("authorize upstream state param error", err)
return err
}
authCodeOptions := []oauth2.AuthCodeOption{ authCodeOptions := []oauth2.AuthCodeOption{
nonceValue.Param(), authRequestState.nonce.Param(),
pkceValue.Challenge(), authRequestState.pkce.Challenge(),
pkceValue.Method(), authRequestState.pkce.Method(),
}
promptParam := r.Form.Get(promptParamName)
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
} }
for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() { for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() {
authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val)) authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val))
} }
if csrfFromCookie == "" {
// We did not receive an incoming CSRF cookie, so write a new one.
err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
if err != nil {
plog.Error("error setting CSRF cookie", err)
return err
}
}
http.Redirect(w, r, http.Redirect(w, r,
upstreamOAuthConfig.AuthCodeURL( upstreamOAuthConfig.AuthCodeURL(
encodedStateParamValue, authRequestState.encodedStateParam,
authCodeOptions..., authCodeOptions...,
), ),
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
@ -291,78 +306,11 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
return nil return nil
} }
func writeAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) error {
if plog.Enabled(plog.LevelTrace) {
// When trace level logging is enabled, include the stack trace in the log message.
keysAndValues := oidc.FositeErrorForLog(err)
errWithStack := errors.WithStack(err)
keysAndValues = append(keysAndValues, "errWithStack")
// klog always prints error values using %s, which does not include stack traces,
// so convert the error to a string which includes the stack trace here.
keysAndValues = append(keysAndValues, fmt.Sprintf("%+v", errWithStack))
plog.Trace("authorize response error", keysAndValues...)
} else {
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
}
if isBrowserless {
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
}
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
return nil
}
func makeDownstreamSessionAndReturnAuthcodeRedirect(
r *http.Request,
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
authorizeRequester fosite.AuthorizeRequester,
subject string,
username string,
groups []string,
customSessionData *psession.CustomSessionData,
) error {
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil {
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, true)
}
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
return nil
}
func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) http.ResponseWriter {
// rewrite http.StatusSeeOther to http.StatusFound for backwards compatibility with old pinniped CLIs.
// we can drop this in a few releases once we feel enough time has passed for users to update.
//
// WriteAuthorizeResponse/WriteAuthorizeError calls used to result in http.StatusFound until
// https://github.com/ory/fosite/pull/636 changed it to http.StatusSeeOther to address
// https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
// Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect.
//
// in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's
// password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther.
return httpsnoop.Wrap(w, httpsnoop.Hooks{
WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
return func(code int) {
if code == http.StatusSeeOther {
code = http.StatusFound
}
delegate(code)
}
},
})
}
func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) { func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester) (string, string, bool) {
username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName) username := r.Header.Get(supervisoroidc.AuthorizeUsernameHeaderName)
password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName) password := r.Header.Get(supervisoroidc.AuthorizePasswordHeaderName)
if username == "" || password == "" { if username == "" || password == "" {
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester, oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester,
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true) fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true)
return "", "", false return "", "", false
} }
@ -372,7 +320,7 @@ func requireNonEmptyUsernameAndPasswordHeaders(r *http.Request, w http.ResponseW
func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, isBrowserless bool) (fosite.AuthorizeRequester, bool) { func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, isBrowserless bool) (fosite.AuthorizeRequester, bool) {
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r) authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r)
if err != nil { if err != nil {
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless) oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless)
return nil, false return nil, false
} }
@ -404,7 +352,8 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken {
return csrfFromCookie return csrfFromCookie
} }
// Select either an OIDC, an LDAP or an AD IDP, or return an error. // chooseUpstreamIDP selects either an OIDC, an LDAP, or an AD IDP, or returns an error.
// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values.
func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) { func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, psession.ProviderType, error) {
oidcUpstreams := idpLister.GetOIDCIdentityProviders() oidcUpstreams := idpLister.GetOIDCIdentityProviders()
ldapUpstreams := idpLister.GetLDAPIdentityProviders() ldapUpstreams := idpLister.GetLDAPIdentityProviders()
@ -440,6 +389,99 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider
} }
} }
type browserFlowAuthRequestState struct {
encodedStateParam string
pkce pkce.Code
nonce nonce.Nonce
}
// handleBrowserFlowAuthRequest performs the shared validations and setup between browser based
// auth requests regardless of IDP type-- LDAP, Active Directory and OIDC.
// It generates the state param, sets the CSRF cookie, and validates the prompt param.
// It returns an error when it encounters an error without handling it, leaving it to
// the caller to decide how to handle it.
// It returns nil with no error when it encounters an error and also has already handled writing
// the error response to the ResponseWriter, in which case the caller should not also try to
// write the error response.
func handleBrowserFlowAuthRequest(
r *http.Request,
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
generateCSRF func() (csrftoken.CSRFToken, error),
generateNonce func() (nonce.Nonce, error),
generatePKCE func() (pkce.Code, error),
upstreamName string,
idpType psession.ProviderType,
cookieCodec oidc.Codec,
upstreamStateEncoder oidc.Encoder,
) (*browserFlowAuthRequestState, error) {
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false)
if !created {
return nil, nil // already wrote the error response, don't return error
}
now := time.Now()
_, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{
Fosite: &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{
// Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations.
Subject: "none",
AuthTime: now,
RequestedAt: now,
},
},
})
if err != nil {
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, false)
return nil, nil // already wrote the error response, don't return error
}
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
if err != nil {
plog.Error("authorize generate error", err)
return nil, err
}
csrfFromCookie := readCSRFCookie(r, cookieCodec)
if csrfFromCookie != "" {
csrfValue = csrfFromCookie
}
encodedStateParamValue, err := upstreamStateParam(
authorizeRequester,
upstreamName,
string(idpType),
nonceValue,
csrfValue,
pkceValue,
upstreamStateEncoder,
)
if err != nil {
plog.Error("authorize upstream state param error", err)
return nil, err
}
promptParam := r.Form.Get(promptParamName)
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
oidc.WriteAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
return nil, nil // already wrote the error response, don't return error
}
if csrfFromCookie == "" {
// We did not receive an incoming CSRF cookie, so write a new one.
err = addCSRFSetCookieHeader(w, csrfValue, cookieCodec)
if err != nil {
plog.Error("error setting CSRF cookie", err)
return nil, err
}
}
return &browserFlowAuthRequestState{
encodedStateParam: encodedStateParamValue,
pkce: pkceValue,
nonce: nonceValue,
}, nil
}
func generateValues( func generateValues(
generateCSRF func() (csrftoken.CSRFToken, error), generateCSRF func() (csrftoken.CSRFToken, error),
generateNonce func() (nonce.Nonce, error), generateNonce func() (nonce.Nonce, error),
@ -463,14 +505,21 @@ func generateValues(
func upstreamStateParam( func upstreamStateParam(
authorizeRequester fosite.AuthorizeRequester, authorizeRequester fosite.AuthorizeRequester,
upstreamName string, upstreamName string,
upstreamType string,
nonceValue nonce.Nonce, nonceValue nonce.Nonce,
csrfValue csrftoken.CSRFToken, csrfValue csrftoken.CSRFToken,
pkceValue pkce.Code, pkceValue pkce.Code,
encoder oidc.Encoder, encoder oidc.Encoder,
) (string, error) { ) (string, error) {
stateParamData := oidc.UpstreamStateParamData{ stateParamData := oidc.UpstreamStateParamData{
AuthParams: authorizeRequester.GetRequestForm().Encode(), // The auth params might have included supervisoroidc.AuthorizeUpstreamIDPNameParamName and
// supervisoroidc.AuthorizeUpstreamIDPTypeParamName, but those can be ignored by other handlers
// that are reading from the encoded upstream state param being built here.
// The UpstreamName and UpstreamType struct fields can be used instead.
// Remove those params here to avoid potential confusion about which should be used later.
AuthParams: removeCustomIDPParams(authorizeRequester.GetRequestForm()).Encode(),
UpstreamName: upstreamName, UpstreamName: upstreamName,
UpstreamType: upstreamType,
Nonce: nonceValue, Nonce: nonceValue,
CSRFToken: csrfValue, CSRFToken: csrfValue,
PKCECode: pkceValue, PKCECode: pkceValue,
@ -483,6 +532,18 @@ func upstreamStateParam(
return encodedStateParamValue, nil return encodedStateParamValue, nil
} }
func removeCustomIDPParams(params url.Values) url.Values {
p := url.Values{}
// Copy all params.
for k, v := range params {
p[k] = v
}
// Remove the unnecessary params.
delete(p, supervisoroidc.AuthorizeUpstreamIDPNameParamName)
delete(p, supervisoroidc.AuthorizeUpstreamIDPTypeParamName)
return p
}
func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken, codec oidc.Encoder) error { func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken, codec oidc.Encoder) error {
encodedCSRFValue, err := codec.Encode(oidc.CSRFCookieEncodingName, csrfValue) encodedCSRFValue, err := codec.Encode(oidc.CSRFCookieEncodingName, csrfValue)
if err != nil { if err != nil {
@ -500,8 +561,3 @@ func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken
return nil return nil
} }
func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string {
ldapURL := *ldapUpstream.GetURL()
return downstreamsession.DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL)
}

View File

@ -70,6 +70,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
downstreamClientID = "pinniped-cli" downstreamClientID = "pinniped-cli"
upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev" upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev"
htmlContentType = "text/html; charset=utf-8" htmlContentType = "text/html; charset=utf-8"
jsonContentType = "application/json; charset=utf-8"
formContentType = "application/x-www-form-urlencoded"
) )
require.Len(t, happyState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case") require.Len(t, happyState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case")
@ -409,23 +411,20 @@ func TestAuthorizationEndpoint(t *testing.T) {
return pathWithQuery("/some/path", modifiedHappyGetRequestQueryMap(queryOverrides)) return pathWithQuery("/some/path", modifiedHappyGetRequestQueryMap(queryOverrides))
} }
expectedUpstreamStateParam := func(queryOverrides map[string]string, csrfValueOverride, upstreamNameOverride string) string { expectedUpstreamStateParam := func(queryOverrides map[string]string, csrfValueOverride, upstreamName, upstreamType string) string {
csrf := happyCSRF csrf := happyCSRF
if csrfValueOverride != "" { if csrfValueOverride != "" {
csrf = csrfValueOverride csrf = csrfValueOverride
} }
upstreamName := oidcUpstreamName
if upstreamNameOverride != "" {
upstreamName = upstreamNameOverride
}
encoded, err := happyStateEncoder.Encode("s", encoded, err := happyStateEncoder.Encode("s",
oidctestutil.ExpectedUpstreamStateParamFormat{ oidctestutil.ExpectedUpstreamStateParamFormat{
P: encodeQuery(modifiedHappyGetRequestQueryMap(queryOverrides)), P: encodeQuery(modifiedHappyGetRequestQueryMap(queryOverrides)),
U: upstreamName, U: upstreamName,
T: upstreamType,
N: happyNonce, N: happyNonce,
C: csrf, C: csrf,
K: happyPKCE, K: happyPKCE,
V: "1", V: "2",
}, },
) )
require.NoError(t, err) require.NoError(t, err)
@ -558,7 +557,41 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true,
},
{
name: "LDAP upstream browser flow happy path using GET without a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: happyGetRequestPath,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", ldapUpstreamName, "ldap")}),
wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true,
},
{
name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: happyGetRequestPath,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", activeDirectoryUpstreamName, "activedirectory")}),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -585,7 +618,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path using GET", name: "LDAP cli upstream happy path using GET",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
@ -606,7 +639,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "ActiveDirectory upstream happy path using GET", name: "ActiveDirectory cli upstream happy path using GET",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
@ -639,7 +672,41 @@ func TestAuthorizationEndpoint(t *testing.T) {
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true,
},
{
name: "LDAP upstream browser flow happy path using GET with a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: happyGetRequestPath,
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ldapUpstreamName, "ldap")}),
wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true,
},
{
name: "Active Directory upstream browser flow happy path using GET with a CSRF cookie",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: happyGetRequestPath,
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, incomingCookieCSRFValue, activeDirectoryUpstreamName, "activedirectory")}),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -653,13 +720,51 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder, cookieEncoder: happyCookieEncoder,
method: http.MethodPost, method: http.MethodPost,
path: "/some/path", path: "/some/path",
contentType: "application/x-www-form-urlencoded", contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap), body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "", wantContentType: "",
wantBodyString: "", wantBodyString: "",
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true,
},
{
name: "LDAP upstream browser flow happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodPost,
path: "/some/path",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: "",
wantBodyString: "",
wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", ldapUpstreamName, "ldap")}),
wantUpstreamStateParamInLocationHeader: true,
},
{
name: "Active Directory upstream browser flow happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodPost,
path: "/some/path",
contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther,
wantContentType: "",
wantBodyString: "",
wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(nil, "", activeDirectoryUpstreamName, "activedirectory")}),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{ {
@ -667,7 +772,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodPost, method: http.MethodPost,
path: "/some/path", path: "/some/path",
contentType: "application/x-www-form-urlencoded", contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap), body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
@ -687,11 +792,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession, wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSession,
}, },
{ {
name: "LDAP upstream happy path using POST", name: "LDAP cli upstream happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodPost, method: http.MethodPost,
path: "/some/path", path: "/some/path",
contentType: "application/x-www-form-urlencoded", contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap), body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
@ -710,11 +815,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
}, },
{ {
name: "Active Directory upstream happy path using POST", name: "Active Directory cli upstream happy path using POST",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodPost, method: http.MethodPost,
path: "/some/path", path: "/some/path",
contentType: "application/x-www-form-urlencoded", contentType: formContentType,
body: encodeQuery(happyGetRequestQueryMap), body: encodeQuery(happyGetRequestQueryMap),
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
@ -742,13 +847,29 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder, cookieEncoder: happyCookieEncoder,
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
contentType: "application/x-www-form-urlencoded",
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), nil), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true,
},
{
name: "OIDC upstream browser flow happy path with custom IDP name and type query params, which are excluded from the query params in the upstream state",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
generateCSRF: happyCSRFGenerator,
generatePKCE: happyPKCEGenerator,
generateNonce: happyNonceGenerator,
stateEncoder: happyStateEncoder,
cookieEncoder: happyCookieEncoder,
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": "currently-ignored", "pinniped_idp_type": "oidc"}),
contentType: formContentType,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyStringWithLocationInHref: true,
wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{ {
@ -761,13 +882,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder, cookieEncoder: happyCookieEncoder,
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}), path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
contentType: "application/x-www-form-urlencoded",
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", oidcUpstreamName, "oidc"), map[string]string{"prompt": "consent", "abc": "123", "def": "456"}),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
}, },
{ {
@ -780,10 +899,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
cookieEncoder: happyCookieEncoder, cookieEncoder: happyCookieEncoder,
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}), path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}),
contentType: "application/x-www-form-urlencoded",
body: encodeQuery(happyGetRequestQueryMap),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -802,7 +919,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: htmlContentType, wantContentType: htmlContentType,
// Generated a new CSRF cookie and set it in the response. // Generated a new CSRF cookie and set it in the response.
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil), wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -823,7 +940,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
}, "", ""), nil), }, "", oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -889,7 +1006,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
"scope": "openid offline_access", "scope": "openid offline_access",
}, "", ""), nil), }, "", oidcUpstreamName, "oidc"), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
}, },
@ -1010,7 +1127,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
Password: "wrong-password", Password: "wrong-password",
}}, }},
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1022,7 +1139,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr("wrong-password"), customPasswordHeader: pointer.StringPtr("wrong-password"),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1034,7 +1151,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr("wrong-password"), customPasswordHeader: pointer.StringPtr("wrong-password"),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1046,7 +1163,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr("wrong-username"), customUsernameHeader: pointer.StringPtr("wrong-username"),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1058,19 +1175,31 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr("wrong-username"), customUsernameHeader: pointer.StringPtr("wrong-username"),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
{ {
name: "missing upstream username on request for LDAP authentication", name: "missing upstream username but has password on request for OIDC password grant",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
method: http.MethodGet,
path: happyGetRequestPath,
customUsernameHeader: nil, // do not send header
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound,
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "",
},
{
name: "missing upstream username but has password on request for LDAP authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: happyGetRequestPath, path: happyGetRequestPath,
customUsernameHeader: nil, // do not send header customUsernameHeader: nil, // do not send header
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1082,7 +1211,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: nil, // do not send header customUsernameHeader: nil, // do not send header
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1094,7 +1223,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: nil, // do not send header customPasswordHeader: nil, // do not send header
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1106,7 +1235,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: nil, // do not send header customPasswordHeader: nil, // do not send header
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1119,7 +1248,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1132,7 +1261,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1145,7 +1274,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1158,7 +1287,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1171,7 +1300,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1184,7 +1313,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1196,7 +1325,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: nil, // do not send header customPasswordHeader: nil, // do not send header
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1208,7 +1337,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithPasswordGrantDisallowedHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1225,7 +1354,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
"redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client",
}), }),
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody, wantBodyJSON: fositeInvalidRedirectURIErrorBody,
}, },
{ {
@ -1238,7 +1367,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody, wantBodyJSON: fositeInvalidRedirectURIErrorBody,
}, },
{ {
@ -1251,7 +1380,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody, wantBodyJSON: fositeInvalidRedirectURIErrorBody,
}, },
{ {
@ -1264,7 +1393,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidRedirectURIErrorBody, wantBodyJSON: fositeInvalidRedirectURIErrorBody,
}, },
{ {
@ -1278,7 +1407,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody, wantBodyJSON: fositeInvalidClientErrorBody,
}, },
{ {
@ -1289,7 +1418,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody, wantBodyJSON: fositeInvalidClientErrorBody,
}, },
{ {
@ -1298,7 +1427,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody, wantBodyJSON: fositeInvalidClientErrorBody,
}, },
{ {
@ -1307,7 +1436,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody, wantBodyJSON: fositeInvalidClientErrorBody,
}, },
{ {
@ -1321,7 +1450,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1333,27 +1462,51 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
{ {
name: "response type is unsupported when using LDAP upstream", name: "response type is unsupported when using LDAP cli upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
{ {
name: "response type is unsupported when using active directory upstream", name: "response type is unsupported when using LDAP browser upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusSeeOther,
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
{
name: "response type is unsupported when using active directory cli upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "",
},
{
name: "response type is unsupported when using active directory browser upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
wantStatus: http.StatusSeeOther,
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1368,7 +1521,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid profile email tuna"}), path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid profile email tuna"}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1380,7 +1533,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1392,7 +1545,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1404,7 +1557,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1419,7 +1572,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1431,27 +1584,51 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
{ {
name: "missing response type in request using LDAP upstream", name: "missing response type in request using LDAP cli upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
{ {
name: "missing response type in request using Active Directory upstream", name: "missing response type in request using LDAP browser upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusSeeOther,
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
{
name: "missing response type in request using Active Directory cli upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "",
},
{
name: "missing response type in request using Active Directory browser upstream",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
wantStatus: http.StatusSeeOther,
wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1466,7 +1643,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}), path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody, wantBodyJSON: fositeInvalidClientErrorBody,
}, },
{ {
@ -1477,7 +1654,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody, wantBodyJSON: fositeInvalidClientErrorBody,
}, },
{ {
@ -1486,7 +1663,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}), path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantBodyJSON: fositeInvalidClientErrorBody, wantBodyJSON: fositeInvalidClientErrorBody,
}, },
{ {
@ -1500,7 +1677,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1513,7 +1690,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1526,7 +1703,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1542,7 +1719,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1555,7 +1732,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1568,7 +1745,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1584,7 +1761,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1597,7 +1774,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1610,7 +1787,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1626,7 +1803,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}), path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1639,7 +1816,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1652,7 +1829,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
@ -1670,7 +1847,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}), path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1685,7 +1862,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error
@ -1700,7 +1877,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantBodyString: "", wantBodyString: "",
wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error
@ -1720,7 +1897,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantCSRFValueInCookieHeader: happyCSRF, wantCSRFValueInCookieHeader: happyCSRF,
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam( wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(
map[string]string{"prompt": "none login", "scope": "email"}, "", "", map[string]string{"prompt": "none login", "scope": "email"}, "", oidcUpstreamName, "oidc",
), nil), ), nil),
wantUpstreamStateParamInLocationHeader: true, wantUpstreamStateParamInLocationHeader: true,
wantBodyStringWithLocationInHref: true, wantBodyStringWithLocationInHref: true,
@ -1889,7 +2066,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithInvalidEmailVerifiedHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithInvalidEmailVerifiedHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1907,7 +2084,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithFalseEmailVerifiedHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithFalseEmailVerifiedHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -1996,7 +2173,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2035,7 +2212,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2050,7 +2227,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2065,7 +2242,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2080,7 +2257,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2095,7 +2272,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2110,7 +2287,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimMissingHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2125,7 +2302,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimEmptyHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2140,7 +2317,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2155,7 +2332,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2170,7 +2347,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2185,7 +2362,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation, wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithRequiredClaimInvalidFormatHintErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2200,7 +2377,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
method: http.MethodGet, method: http.MethodGet,
path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}), path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}),
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2212,7 +2389,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername), customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword), customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2224,7 +2401,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
customPasswordHeader: pointer.StringPtr(happyLDAPPassword), customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
wantStatus: http.StatusFound, wantStatus: http.StatusFound,
wantContentType: "application/json; charset=utf-8", wantContentType: jsonContentType,
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
wantBodyString: "", wantBodyString: "",
}, },
@ -2401,7 +2578,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
require.Equal(t, test.wantStatus, rsp.Code) require.Equal(t, test.wantStatus, rsp.Code)
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType) testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType)
testutil.RequireSecurityHeaders(t, rsp) testutil.RequireSecurityHeadersWithoutCustomCSPs(t, rsp)
if test.wantPasswordGrantCall != nil { if test.wantPasswordGrantCall != nil {
test.wantPasswordGrantCall.args.Ctx = reqContext test.wantPasswordGrantCall.args.Ctx = reqContext
@ -2543,7 +2720,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
"scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation "scope": "some-other-new-scope1 some-other-new-scope2", // updated expectation
"client_id": "some-other-new-client-id", // updated expectation "client_id": "some-other-new-client-id", // updated expectation
"state": expectedUpstreamStateParam( "state": expectedUpstreamStateParam(
nil, "", "some-other-new-idp-name", nil, "", "some-other-new-idp-name", "oidc",
), // updated expectation ), // updated expectation
"nonce": happyNonce, "nonce": happyNonce,
"code_challenge": expectedUpstreamCodeChallenge, "code_challenge": expectedUpstreamCodeChallenge,

View File

@ -5,7 +5,6 @@
package callback package callback
import ( import (
"crypto/subtle"
"net/http" "net/http"
"net/url" "net/url"
@ -14,7 +13,6 @@ import (
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/httputil/securityheader"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/csrftoken"
"go.pinniped.dev/internal/oidc/downstreamsession" "go.pinniped.dev/internal/oidc/downstreamsession"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/oidc/provider/formposthtml"
@ -102,9 +100,9 @@ func validateRequest(r *http.Request, stateDecoder, cookieDecoder oidc.Decoder)
return nil, httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET)", r.Method) return nil, httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET)", r.Method)
} }
csrfValue, err := readCSRFCookie(r, cookieDecoder) _, decodedState, err := oidc.ReadStateParamAndValidateCSRFCookie(r, cookieDecoder, stateDecoder)
if err != nil { if err != nil {
plog.InfoErr("error reading CSRF cookie", err) plog.InfoErr("state or CSRF error", err)
return nil, err return nil, err
} }
@ -113,23 +111,7 @@ func validateRequest(r *http.Request, stateDecoder, cookieDecoder oidc.Decoder)
return nil, httperr.New(http.StatusBadRequest, "code param not found") return nil, httperr.New(http.StatusBadRequest, "code param not found")
} }
if r.FormValue("state") == "" { return decodedState, nil
plog.Info("state param not found")
return nil, httperr.New(http.StatusBadRequest, "state param not found")
}
state, err := readState(r, stateDecoder)
if err != nil {
plog.InfoErr("error reading state", err)
return nil, err
}
if subtle.ConstantTimeCompare([]byte(state.CSRFToken), []byte(csrfValue)) != 1 {
plog.InfoErr("CSRF value does not match", err)
return nil, httperr.Wrap(http.StatusForbidden, "CSRF value does not match", err)
}
return state, nil
} }
func findUpstreamIDPConfig(upstreamName string, upstreamIDPs oidc.UpstreamOIDCIdentityProvidersLister) provider.UpstreamOIDCIdentityProviderI { func findUpstreamIDPConfig(upstreamName string, upstreamIDPs oidc.UpstreamOIDCIdentityProvidersLister) provider.UpstreamOIDCIdentityProviderI {
@ -140,36 +122,3 @@ func findUpstreamIDPConfig(upstreamName string, upstreamIDPs oidc.UpstreamOIDCId
} }
return nil return nil
} }
func readCSRFCookie(r *http.Request, cookieDecoder oidc.Decoder) (csrftoken.CSRFToken, error) {
receivedCSRFCookie, err := r.Cookie(oidc.CSRFCookieName)
if err != nil {
// Error means that the cookie was not found
return "", httperr.Wrap(http.StatusForbidden, "CSRF cookie is missing", err)
}
var csrfFromCookie csrftoken.CSRFToken
err = cookieDecoder.Decode(oidc.CSRFCookieEncodingName, receivedCSRFCookie.Value, &csrfFromCookie)
if err != nil {
return "", httperr.Wrap(http.StatusForbidden, "error reading CSRF cookie", err)
}
return csrfFromCookie, nil
}
func readState(r *http.Request, stateDecoder oidc.Decoder) (*oidc.UpstreamStateParamData, error) {
var state oidc.UpstreamStateParamData
if err := stateDecoder.Decode(
oidc.UpstreamStateParamEncodingName,
r.FormValue("state"),
&state,
); err != nil {
return nil, httperr.New(http.StatusBadRequest, "error reading state")
}
if state.FormatVersion != oidc.UpstreamStateParamFormatVersion {
return nil, httperr.New(http.StatusUnprocessableEntity, "state format version is invalid")
}
return &state, nil
}

View File

@ -48,7 +48,7 @@ const (
happyDownstreamCSRF = "test-csrf" happyDownstreamCSRF = "test-csrf"
happyDownstreamPKCE = "test-pkce" happyDownstreamPKCE = "test-pkce"
happyDownstreamNonce = "test-nonce" happyDownstreamNonce = "test-nonce"
happyDownstreamStateVersion = "1" happyDownstreamStateVersion = "2"
downstreamIssuer = "https://my-downstream-issuer.com/path" downstreamIssuer = "https://my-downstream-issuer.com/path"
downstreamRedirectURI = "http://127.0.0.1/callback" downstreamRedirectURI = "http://127.0.0.1/callback"
@ -1034,7 +1034,7 @@ func TestCallbackEndpoint(t *testing.T) {
t.Logf("response: %#v", rsp) t.Logf("response: %#v", rsp)
t.Logf("response body: %q", rsp.Body.String()) t.Logf("response body: %q", rsp.Body.String())
testutil.RequireSecurityHeaders(t, rsp) testutil.RequireSecurityHeadersWithFormPostPageCSPs(t, rsp)
if test.wantAuthcodeExchangeCall != nil { if test.wantAuthcodeExchangeCall != nil {
test.wantAuthcodeExchangeCall.args.Ctx = reqContext test.wantAuthcodeExchangeCall.args.Ctx = reqContext
@ -1156,12 +1156,11 @@ func (r *requestPath) String() string {
return path + params.Encode() return path + params.Encode()
} }
type upstreamStateParamBuilder oidctestutil.ExpectedUpstreamStateParamFormat func happyUpstreamStateParam() *oidctestutil.UpstreamStateParamBuilder {
return &oidctestutil.UpstreamStateParamBuilder{
func happyUpstreamStateParam() *upstreamStateParamBuilder {
return &upstreamStateParamBuilder{
U: happyUpstreamIDPName, U: happyUpstreamIDPName,
P: happyDownstreamRequestParams, P: happyDownstreamRequestParams,
T: "oidc",
N: happyDownstreamNonce, N: happyDownstreamNonce,
C: happyDownstreamCSRF, C: happyDownstreamCSRF,
K: happyDownstreamPKCE, K: happyDownstreamPKCE,
@ -1169,37 +1168,6 @@ func happyUpstreamStateParam() *upstreamStateParamBuilder {
} }
} }
func (b upstreamStateParamBuilder) Build(t *testing.T, stateEncoder *securecookie.SecureCookie) string {
state, err := stateEncoder.Encode("s", b)
require.NoError(t, err)
return state
}
func (b *upstreamStateParamBuilder) WithAuthorizeRequestParams(params string) *upstreamStateParamBuilder {
b.P = params
return b
}
func (b *upstreamStateParamBuilder) WithNonce(nonce string) *upstreamStateParamBuilder {
b.N = nonce
return b
}
func (b *upstreamStateParamBuilder) WithCSRF(csrf string) *upstreamStateParamBuilder {
b.C = csrf
return b
}
func (b *upstreamStateParamBuilder) WithPKCVE(pkce string) *upstreamStateParamBuilder {
b.K = pkce
return b
}
func (b *upstreamStateParamBuilder) WithStateVersion(version string) *upstreamStateParamBuilder {
b.V = version
return b
}
func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(happyUpstreamIDPName). WithName(happyUpstreamIDPName).

View File

@ -16,6 +16,7 @@ import (
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
@ -61,6 +62,34 @@ func MakeDownstreamSession(subject string, username string, groups []string, cus
return openIDSession return openIDSession
} }
func MakeDownstreamLDAPOrADCustomSessionData(
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
idpType psession.ProviderType,
authenticateResponse *authenticators.Response,
) *psession.CustomSessionData {
customSessionData := &psession.CustomSessionData{
ProviderUID: ldapUpstream.GetResourceUID(),
ProviderName: ldapUpstream.GetName(),
ProviderType: idpType,
}
if idpType == psession.ProviderTypeLDAP {
customSessionData.LDAP = &psession.LDAPSessionData{
UserDN: authenticateResponse.DN,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
if idpType == psession.ProviderTypeActiveDirectory {
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
UserDN: authenticateResponse.DN,
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
}
}
return customSessionData
}
func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token) (*psession.CustomSessionData, error) { func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token) (*psession.CustomSessionData, error) {
upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, oidcUpstream.GetName(), token.IDToken.Claims) upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, oidcUpstream.GetName(), token.IDToken.Claims)
if err != nil { if err != nil {
@ -228,6 +257,11 @@ func ExtractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
return valueAsString, nil return valueAsString, nil
} }
func DownstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string {
ldapURL := *ldapUpstream.GetURL()
return DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL)
}
func DownstreamLDAPSubject(uid string, ldapURL url.URL) string { func DownstreamLDAPSubject(uid string, ldapURL url.URL) string {
q := ldapURL.Query() q := ldapURL.Query()
q.Set(oidc.IDTokenSubjectClaim, uid) q.Set(oidc.IDTokenSubjectClaim, uid)

View File

@ -1,4 +1,4 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved. // Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Package idpdiscovery provides a handler for the upstream IDP discovery endpoint. // Package idpdiscovery provides a handler for the upstream IDP discovery endpoint.
@ -44,14 +44,14 @@ func responseAsJSON(upstreamIDPs oidc.UpstreamIdentityProvidersLister) ([]byte,
r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{
Name: provider.GetName(), Name: provider.GetName(),
Type: v1alpha1.IDPTypeLDAP, Type: v1alpha1.IDPTypeLDAP,
Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword}, Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode},
}) })
} }
for _, provider := range upstreamIDPs.GetActiveDirectoryIdentityProviders() { for _, provider := range upstreamIDPs.GetActiveDirectoryIdentityProviders() {
r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{
Name: provider.GetName(), Name: provider.GetName(),
Type: v1alpha1.IDPTypeActiveDirectory, Type: v1alpha1.IDPTypeActiveDirectory,
Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword}, Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode},
}) })
} }
for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() { for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() {

View File

@ -1,4 +1,4 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved. // Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package idpdiscovery package idpdiscovery
@ -37,22 +37,22 @@ func TestIDPDiscovery(t *testing.T) {
wantContentType: "application/json", wantContentType: "application/json",
wantFirstResponseBodyJSON: here.Doc(`{ wantFirstResponseBodyJSON: here.Doc(`{
"pinniped_identity_providers": [ "pinniped_identity_providers": [
{"name": "a-some-ldap-idp", "type": "ldap", "flows": ["cli_password"]}, {"name": "a-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]},
{"name": "a-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode"]}, {"name": "a-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode"]},
{"name": "x-some-idp", "type": "ldap", "flows": ["cli_password"]}, {"name": "x-some-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]},
{"name": "x-some-idp", "type": "oidc", "flows": ["browser_authcode"]}, {"name": "x-some-idp", "type": "oidc", "flows": ["browser_authcode"]},
{"name": "y-some-ad-idp", "type": "activedirectory", "flows": ["cli_password"]}, {"name": "y-some-ad-idp", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]},
{"name": "z-some-ad-idp", "type": "activedirectory", "flows": ["cli_password"]}, {"name": "z-some-ad-idp", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]},
{"name": "z-some-ldap-idp", "type": "ldap", "flows": ["cli_password"]}, {"name": "z-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]},
{"name": "z-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode", "cli_password"]} {"name": "z-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode", "cli_password"]}
] ]
}`), }`),
wantSecondResponseBodyJSON: here.Doc(`{ wantSecondResponseBodyJSON: here.Doc(`{
"pinniped_identity_providers": [ "pinniped_identity_providers": [
{"name": "some-other-ad-idp-1", "type": "activedirectory", "flows": ["cli_password"]}, {"name": "some-other-ad-idp-1", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]},
{"name": "some-other-ad-idp-2", "type": "activedirectory", "flows": ["cli_password"]}, {"name": "some-other-ad-idp-2", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]},
{"name": "some-other-ldap-idp-1", "type": "ldap", "flows": ["cli_password"]}, {"name": "some-other-ldap-idp-1", "type": "ldap", "flows": ["cli_password", "browser_authcode"]},
{"name": "some-other-ldap-idp-2", "type": "ldap", "flows": ["cli_password"]}, {"name": "some-other-ldap-idp-2", "type": "ldap", "flows": ["cli_password", "browser_authcode"]},
{"name": "some-other-oidc-idp-1", "type": "oidc", "flows": ["browser_authcode", "cli_password"]}, {"name": "some-other-oidc-idp-1", "type": "oidc", "flows": ["browser_authcode", "cli_password"]},
{"name": "some-other-oidc-idp-2", "type": "oidc", "flows": ["browser_authcode"]} {"name": "some-other-oidc-idp-2", "type": "oidc", "flows": ["browser_authcode"]}
] ]

View File

@ -0,0 +1,42 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"net/http"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/login/loginhtml"
)
const (
internalErrorMessage = "An internal error occurred. Please contact your administrator for help."
incorrectUsernameOrPasswordErrorMessage = "Incorrect username or password."
)
func NewGetHandler(loginPath string) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error {
alertMessage, hasAlert := getAlert(r)
pageInputs := &loginhtml.PageData{
PostPath: loginPath,
State: encodedState,
IDPName: decodedState.UpstreamName,
HasAlertError: hasAlert,
AlertMessage: alertMessage,
}
return loginhtml.Template().Execute(w, pageInputs)
}
}
func getAlert(r *http.Request) (string, bool) {
errorParamValue := r.URL.Query().Get(errParamName)
message := internalErrorMessage
if errorParamValue == string(ShowBadUserPassErr) {
message = incorrectUsernameOrPasswordErrorMessage
}
return message, errorParamValue != ""
}

View File

@ -0,0 +1,116 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/login/loginhtml"
"go.pinniped.dev/internal/testutil"
)
func TestGetLogin(t *testing.T) {
const (
testPath = "/some/path/login"
testUpstreamName = "some-ldap-idp"
testUpstreamType = "ldap"
testEncodedState = "fake-encoded-state-value"
)
tests := []struct {
name string
decodedState *oidc.UpstreamStateParamData
encodedState string
errParam string
idps oidc.UpstreamIdentityProvidersLister
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Happy path ldap",
decodedState: &oidc.UpstreamStateParamData{
UpstreamName: testUpstreamName,
UpstreamType: testUpstreamType,
},
encodedState: testEncodedState, // the encoded and decoded state don't match, but that verification is handled one level up.
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState, ""), // no alert message
},
{
name: "displays error banner when err=login_error param is sent",
decodedState: &oidc.UpstreamStateParamData{
UpstreamName: testUpstreamName,
UpstreamType: testUpstreamType,
},
encodedState: testEncodedState,
errParam: "login_error",
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState,
"Incorrect username or password.",
),
},
{
name: "displays error banner when err=internal_error param is sent",
decodedState: &oidc.UpstreamStateParamData{
UpstreamName: testUpstreamName,
UpstreamType: testUpstreamType,
},
encodedState: testEncodedState,
errParam: "internal_error",
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState,
"An internal error occurred. Please contact your administrator for help.",
),
},
{
// If we get an error that we don't recognize, that's also an error, so we
// should probably just tell you to contact your administrator...
name: "displays generic error banner when unrecognized err param is sent",
decodedState: &oidc.UpstreamStateParamData{
UpstreamName: testUpstreamName,
UpstreamType: testUpstreamType,
},
encodedState: testEncodedState,
errParam: "some_other_error",
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: testutil.ExpectedLoginPageHTML(loginhtml.CSS(), testUpstreamName, testPath, testEncodedState,
"An internal error occurred. Please contact your administrator for help.",
),
},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
handler := NewGetHandler(testPath)
target := testPath + "?state=" + tt.encodedState
if tt.errParam != "" {
target += "&err=" + tt.errParam
}
req := httptest.NewRequest(http.MethodGet, target, nil)
rsp := httptest.NewRecorder()
err := handler(rsp, req, tt.encodedState, tt.decodedState)
require.NoError(t, err)
require.Equal(t, tt.wantStatus, rsp.Code)
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType)
body := rsp.Body.String()
// t.Log("actual body:", body) // useful when updating expected values
require.Equal(t, tt.wantBody, body)
})
}
}

View File

@ -0,0 +1,125 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"net/http"
"net/url"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/httputil/securityheader"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/login/loginhtml"
"go.pinniped.dev/internal/oidc/provider/formposthtml"
"go.pinniped.dev/internal/plog"
)
type ErrorParamValue string
const (
usernameParamName = "username"
passwordParamName = "password"
stateParamName = "state"
errParamName = "err"
ShowNoError ErrorParamValue = ""
ShowInternalError ErrorParamValue = "internal_error"
ShowBadUserPassErr ErrorParamValue = "login_error"
)
// HandlerFunc is a function that can handle either a GET or POST request for the login endpoint.
type HandlerFunc func(
w http.ResponseWriter,
r *http.Request,
encodedState string,
decodedState *oidc.UpstreamStateParamData,
) error
// NewHandler returns a http.Handler that serves the login endpoint for IDPs that don't have their own web UI for login.
//
// This handler takes care of the shared concerns between the GET and POST methods of the login endpoint:
// checking the method, checking the CSRF cookie, decoding the state param, and adding security headers.
// Then it defers the rest of the handling to the passed in handler functions for GET and POST requests.
// Note that CSRF protection isn't needed on GET requests, but it doesn't hurt. Putting it here
// keeps the implementations and tests of HandlerFunc simpler since they won't need to deal with any decoders.
// Users should always initially get redirected to this page from the authorization endpoint, and never need
// to navigate directly to this page in their browser without going through the authorization endpoint first.
// Once their browser has landed on this page, it should be okay for the user to refresh the browser.
func NewHandler(
stateDecoder oidc.Decoder,
cookieDecoder oidc.Decoder,
getHandler HandlerFunc, // use NewGetHandler() for production
postHandler HandlerFunc, // use NewPostHandler() for production
) http.Handler {
loginHandler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
var handler HandlerFunc
switch r.Method {
case http.MethodGet:
handler = getHandler
case http.MethodPost:
handler = postHandler
default:
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
}
encodedState, decodedState, err := oidc.ReadStateParamAndValidateCSRFCookie(r, cookieDecoder, stateDecoder)
if err != nil {
plog.InfoErr("state or CSRF error", err)
return err
}
switch decodedState.UpstreamType {
case string(idpdiscoveryv1alpha1.IDPTypeLDAP), string(idpdiscoveryv1alpha1.IDPTypeActiveDirectory):
// these are the types supported by this endpoint, so no error here
default:
return httperr.Newf(http.StatusBadRequest, "not a supported upstream IDP type for this endpoint: %q", decodedState.UpstreamType)
}
return handler(w, r, encodedState, decodedState)
})
return wrapSecurityHeaders(loginHandler)
}
func wrapSecurityHeaders(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wrapped := securityheader.WrapWithCustomCSP(handler, loginhtml.ContentSecurityPolicy())
if r.Method == http.MethodPost {
// POST requests can result in the form_post html page, so allow it with CSP headers.
wrapped = securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy())
}
wrapped.ServeHTTP(w, r)
})
}
// RedirectToLoginPage redirects to the GET /login page of the specified issuer.
// The specified issuer should never end with a "/", which is validated by
// provider.FederationDomainIssuer when the issuer string comes from that type.
func RedirectToLoginPage(
r *http.Request,
w http.ResponseWriter,
downstreamIssuer string,
encodedStateParamValue string,
errToDisplay ErrorParamValue,
) error {
loginURL, err := url.Parse(downstreamIssuer + oidc.PinnipedLoginPath)
if err != nil {
return err
}
q := loginURL.Query()
q.Set(stateParamName, encodedStateParamValue)
if errToDisplay != ShowNoError {
q.Set(errParamName, string(errToDisplay))
}
loginURL.RawQuery = q.Encode()
http.Redirect(w, r,
loginURL.String(),
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
)
return nil
}

View File

@ -0,0 +1,457 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/gorilla/securecookie"
"github.com/stretchr/testify/require"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil"
)
const (
htmlContentType = "text/html; charset=utf-8"
)
func TestLoginEndpoint(t *testing.T) {
const (
happyGetResult = "<p>get handler result</p>"
happyPostResult = "<p>post handler result</p>"
happyUpstreamIDPName = "upstream-idp-name"
happyUpstreamIDPType = "ldap"
happyDownstreamCSRF = "test-csrf"
happyDownstreamPKCE = "test-pkce"
happyDownstreamNonce = "test-nonce"
happyDownstreamStateVersion = "2"
downstreamClientID = "pinniped-cli"
happyDownstreamState = "8b-state"
downstreamNonce = "some-nonce-value"
downstreamPKCEChallenge = "some-challenge"
downstreamPKCEChallengeMethod = "S256"
downstreamRedirectURI = "http://127.0.0.1/callback"
)
happyDownstreamScopesRequested := []string{"openid"}
happyDownstreamRequestParamsQuery := url.Values{
"response_type": []string{"code"},
"scope": []string{strings.Join(happyDownstreamScopesRequested, " ")},
"client_id": []string{downstreamClientID},
"state": []string{happyDownstreamState},
"nonce": []string{downstreamNonce},
"code_challenge": []string{downstreamPKCEChallenge},
"code_challenge_method": []string{downstreamPKCEChallengeMethod},
"redirect_uri": []string{downstreamRedirectURI},
}
happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode()
expectedHappyDecodedUpstreamStateParam := func() *oidc.UpstreamStateParamData {
return &oidc.UpstreamStateParamData{
UpstreamName: happyUpstreamIDPName,
UpstreamType: happyUpstreamIDPType,
AuthParams: happyDownstreamRequestParams,
Nonce: happyDownstreamNonce,
CSRFToken: happyDownstreamCSRF,
PKCECode: happyDownstreamPKCE,
FormatVersion: happyDownstreamStateVersion,
}
}
expectedHappyDecodedUpstreamStateParamForActiveDirectory := func() *oidc.UpstreamStateParamData {
s := expectedHappyDecodedUpstreamStateParam()
s.UpstreamType = "activedirectory"
return s
}
happyUpstreamStateParam := func() *oidctestutil.UpstreamStateParamBuilder {
return &oidctestutil.UpstreamStateParamBuilder{
U: happyUpstreamIDPName,
T: happyUpstreamIDPType,
P: happyDownstreamRequestParams,
N: happyDownstreamNonce,
C: happyDownstreamCSRF,
K: happyDownstreamPKCE,
V: happyDownstreamStateVersion,
}
}
stateEncoderHashKey := []byte("fake-hash-secret")
stateEncoderBlockKey := []byte("0123456789ABCDEF") // block encryption requires 16/24/32 bytes for AES
cookieEncoderHashKey := []byte("fake-hash-secret2")
cookieEncoderBlockKey := []byte("0123456789ABCDE2") // block encryption requires 16/24/32 bytes for AES
require.NotEqual(t, stateEncoderHashKey, cookieEncoderHashKey)
require.NotEqual(t, stateEncoderBlockKey, cookieEncoderBlockKey)
happyStateCodec := securecookie.New(stateEncoderHashKey, stateEncoderBlockKey)
happyStateCodec.SetSerializer(securecookie.JSONEncoder{})
happyCookieCodec := securecookie.New(cookieEncoderHashKey, cookieEncoderBlockKey)
happyCookieCodec.SetSerializer(securecookie.JSONEncoder{})
happyState := happyUpstreamStateParam().Build(t, happyStateCodec)
happyPathWithState := newRequestPath().WithState(happyState).String()
happyActiveDirectoryState := happyUpstreamStateParam().WithUpstreamIDPType("activedirectory").Build(t, happyStateCodec)
encodedIncomingCookieCSRFValue, err := happyCookieCodec.Encode("csrf", happyDownstreamCSRF)
require.NoError(t, err)
happyCSRFCookie := "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue
tests := []struct {
name string
method string
path string
csrfCookie string
getHandlerErr error
postHandlerErr error
wantStatus int
wantContentType string
wantBody string
wantEncodedState string
wantDecodedState *oidc.UpstreamStateParamData
}{
{
name: "PUT method is invalid",
method: http.MethodPut,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusMethodNotAllowed,
wantContentType: htmlContentType,
wantBody: "Method Not Allowed: PUT (try GET or POST)\n",
},
{
name: "PATCH method is invalid",
method: http.MethodPatch,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusMethodNotAllowed,
wantContentType: htmlContentType,
wantBody: "Method Not Allowed: PATCH (try GET or POST)\n",
},
{
name: "DELETE method is invalid",
method: http.MethodDelete,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusMethodNotAllowed,
wantContentType: htmlContentType,
wantBody: "Method Not Allowed: DELETE (try GET or POST)\n",
},
{
name: "HEAD method is invalid",
method: http.MethodHead,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusMethodNotAllowed,
wantContentType: htmlContentType,
wantBody: "Method Not Allowed: HEAD (try GET or POST)\n",
},
{
name: "CONNECT method is invalid",
method: http.MethodConnect,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusMethodNotAllowed,
wantContentType: htmlContentType,
wantBody: "Method Not Allowed: CONNECT (try GET or POST)\n",
},
{
name: "OPTIONS method is invalid",
method: http.MethodOptions,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusMethodNotAllowed,
wantContentType: htmlContentType,
wantBody: "Method Not Allowed: OPTIONS (try GET or POST)\n",
},
{
name: "TRACE method is invalid",
method: http.MethodTrace,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusMethodNotAllowed,
wantContentType: htmlContentType,
wantBody: "Method Not Allowed: TRACE (try GET or POST)\n",
},
{
name: "state param was not included on GET request",
method: http.MethodGet,
path: newRequestPath().WithoutState().String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: state param not found\n",
},
{
name: "state param was not included on POST request",
method: http.MethodPost,
path: newRequestPath().WithoutState().String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: state param not found\n",
},
{
name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason on GET request",
method: http.MethodGet,
path: newRequestPath().WithState("this-will-not-decode").String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: error reading state\n",
},
{
name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason on POST request",
method: http.MethodPost,
path: newRequestPath().WithState("this-will-not-decode").String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: error reading state\n",
},
{
name: "the CSRF cookie does not exist on GET request",
method: http.MethodGet,
path: happyPathWithState,
csrfCookie: "",
wantStatus: http.StatusForbidden,
wantContentType: htmlContentType,
wantBody: "Forbidden: CSRF cookie is missing\n",
},
{
name: "the CSRF cookie does not exist on POST request",
method: http.MethodPost,
path: happyPathWithState,
csrfCookie: "",
wantStatus: http.StatusForbidden,
wantContentType: htmlContentType,
wantBody: "Forbidden: CSRF cookie is missing\n",
},
{
name: "the CSRF cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason on GET request",
method: http.MethodGet,
path: happyPathWithState,
csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped",
wantStatus: http.StatusForbidden,
wantContentType: htmlContentType,
wantBody: "Forbidden: error reading CSRF cookie\n",
},
{
name: "the CSRF cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason on POST request",
method: http.MethodPost,
path: happyPathWithState,
csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped",
wantStatus: http.StatusForbidden,
wantContentType: htmlContentType,
wantBody: "Forbidden: error reading CSRF cookie\n",
},
{
name: "cookie csrf value does not match state csrf value on GET request",
method: http.MethodGet,
path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusForbidden,
wantContentType: htmlContentType,
wantBody: "Forbidden: CSRF value does not match\n",
},
{
name: "cookie csrf value does not match state csrf value on POST request",
method: http.MethodPost,
path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusForbidden,
wantContentType: htmlContentType,
wantBody: "Forbidden: CSRF value does not match\n",
},
{
name: "GET request when upstream IDP type in state param is not supported by this endpoint",
method: http.MethodGet,
path: newRequestPath().WithState(
happyUpstreamStateParam().WithUpstreamIDPType("oidc").Build(t, happyStateCodec),
).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: not a supported upstream IDP type for this endpoint: \"oidc\"\n",
},
{
name: "POST request when upstream IDP type in state param is not supported by this endpoint",
method: http.MethodPost,
path: newRequestPath().WithState(
happyUpstreamStateParam().WithUpstreamIDPType("oidc").Build(t, happyStateCodec),
).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: not a supported upstream IDP type for this endpoint: \"oidc\"\n",
},
{
name: "valid GET request when GET endpoint handler returns an error",
method: http.MethodGet,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
getHandlerErr: httperr.Newf(http.StatusInternalServerError, "some get error"),
wantStatus: http.StatusInternalServerError,
wantContentType: htmlContentType,
wantBody: "Internal Server Error: some get error\n",
wantEncodedState: happyState,
wantDecodedState: expectedHappyDecodedUpstreamStateParam(),
},
{
name: "valid POST request when POST endpoint handler returns an error",
method: http.MethodPost,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
postHandlerErr: httperr.Newf(http.StatusInternalServerError, "some post error"),
wantStatus: http.StatusInternalServerError,
wantContentType: htmlContentType,
wantBody: "Internal Server Error: some post error\n",
wantEncodedState: happyState,
wantDecodedState: expectedHappyDecodedUpstreamStateParam(),
},
{
name: "happy GET request for LDAP upstream",
method: http.MethodGet,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: happyGetResult,
wantEncodedState: happyState,
wantDecodedState: expectedHappyDecodedUpstreamStateParam(),
},
{
name: "happy POST request for LDAP upstream",
method: http.MethodPost,
path: happyPathWithState,
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: happyPostResult,
wantEncodedState: happyState,
wantDecodedState: expectedHappyDecodedUpstreamStateParam(),
},
{
name: "happy GET request for ActiveDirectory upstream",
method: http.MethodGet,
path: newRequestPath().WithState(happyActiveDirectoryState).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: happyGetResult,
wantEncodedState: happyActiveDirectoryState,
wantDecodedState: expectedHappyDecodedUpstreamStateParamForActiveDirectory(),
},
{
name: "happy POST request for ActiveDirectory upstream",
method: http.MethodPost,
path: newRequestPath().WithState(happyActiveDirectoryState).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBody: happyPostResult,
wantEncodedState: happyActiveDirectoryState,
wantDecodedState: expectedHappyDecodedUpstreamStateParamForActiveDirectory(),
},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(tt.method, tt.path, nil)
if tt.csrfCookie != "" {
req.Header.Set("Cookie", tt.csrfCookie)
}
rsp := httptest.NewRecorder()
testGetHandler := func(
w http.ResponseWriter,
r *http.Request,
encodedState string,
decodedState *oidc.UpstreamStateParamData,
) error {
require.Equal(t, req, r)
require.Equal(t, rsp, w)
require.Equal(t, tt.wantEncodedState, encodedState)
require.Equal(t, tt.wantDecodedState, decodedState)
if tt.getHandlerErr == nil {
_, err := w.Write([]byte(happyGetResult))
require.NoError(t, err)
}
return tt.getHandlerErr
}
testPostHandler := func(
w http.ResponseWriter,
r *http.Request,
encodedState string,
decodedState *oidc.UpstreamStateParamData,
) error {
require.Equal(t, req, r)
require.Equal(t, rsp, w)
require.Equal(t, tt.wantEncodedState, encodedState)
require.Equal(t, tt.wantDecodedState, decodedState)
if tt.postHandlerErr == nil {
_, err := w.Write([]byte(happyPostResult))
require.NoError(t, err)
}
return tt.postHandlerErr
}
subject := NewHandler(happyStateCodec, happyCookieCodec, testGetHandler, testPostHandler)
subject.ServeHTTP(rsp, req)
if tt.method == http.MethodPost {
testutil.RequireSecurityHeadersWithFormPostPageCSPs(t, rsp)
} else {
testutil.RequireSecurityHeadersWithLoginPageCSPs(t, rsp)
}
require.Equal(t, tt.wantStatus, rsp.Code)
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType)
require.Equal(t, tt.wantBody, rsp.Body.String())
})
}
}
type requestPath struct {
state *string
}
func newRequestPath() *requestPath {
return &requestPath{}
}
func (r *requestPath) WithState(state string) *requestPath {
r.state = &state
return r
}
func (r *requestPath) WithoutState() *requestPath {
r.state = nil
return r
}
func (r *requestPath) String() string {
path := "/login?"
params := url.Values{}
if r.state != nil {
params.Add("state", *r.state)
}
return path + params.Encode()
}

View File

@ -0,0 +1,94 @@
/* Copyright 2022 the Pinniped contributors. All Rights Reserved. */
/* SPDX-License-Identifier: Apache-2.0 */
html {
height: 100%;
}
body {
font-family: "Metropolis-Light", Helvetica, sans-serif;
display: flex;
flex-flow: column wrap;
justify-content: flex-start;
align-items: center;
/* subtle gradient make the login box stand out */
background: linear-gradient(to top, #f8f8f8, white);
min-height: 100%;
}
h1 {
font-size: 20px;
margin: 0;
}
.box {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
border-radius: 4px;
border-color: #ddd;
border-width: 1px;
border-style: solid;
width: 400px;
padding:30px 30px 0;
margin: 60px 20px 0;
background: white;
font-size: 14px;
}
input {
color: inherit;
font: inherit;
border: 0;
margin: 0;
outline: 0;
padding: 0;
}
.form-field {
display: flex;
margin-bottom: 30px;
}
.form-field input[type="password"], .form-field input[type="text"], .form-field input[type="submit"] {
width: 100%;
padding: 1em;
}
.form-field input[type="password"], .form-field input[type="text"] {
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: #a6a6a6;
}
.form-field input[type="submit"] {
background-color: #218fcf; /* this is a color from the Pinniped logo :) */
color: #eee;
font-weight: bold;
cursor: pointer;
transition: all .3s;
}
.form-field input[type="submit"]:focus, .form-field input[type="submit"]:hover {
background-color: #1abfd3; /* this is a color from the Pinniped logo :) */
}
.form-field input[type="submit"]:active {
transform: scale(.99);
}
.hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.alert {
color: crimson;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,65 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package loginhtml defines HTML templates used by the Supervisor.
//nolint: gochecknoglobals // This package uses globals to ensure that all parsing and minifying happens at init.
package loginhtml
import (
_ "embed" // Needed to trigger //go:embed directives below.
"html/template"
"strings"
"github.com/tdewolff/minify/v2/minify"
"go.pinniped.dev/internal/oidc/provider/csp"
)
var (
//go:embed login_form.css
rawCSS string
minifiedCSS = panicOnError(minify.CSS(rawCSS))
//go:embed login_form.gohtml
rawHTMLTemplate string
)
// Parse the Go templated HTML and inject functions providing the minified inline CSS and JS.
var parsedHTMLTemplate = template.Must(template.New("login_form.gohtml").Funcs(template.FuncMap{
"minifiedCSS": func() template.CSS { return template.CSS(CSS()) },
}).Parse(rawHTMLTemplate))
// Generate the CSP header value once since it's effectively constant.
var cspValue = strings.Join([]string{
`default-src 'none'`,
`style-src '` + csp.Hash(minifiedCSS) + `'`,
`frame-ancestors 'none'`,
}, "; ")
func panicOnError(s string, err error) string {
if err != nil {
panic(err)
}
return s
}
// ContentSecurityPolicy returns the Content-Security-Policy header value to make the Template() operate correctly.
//
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy.
func ContentSecurityPolicy() string { return cspValue }
// Template returns the html/template.Template for rendering the login page.
func Template() *template.Template { return parsedHTMLTemplate }
// CSS returns the minified CSS that will be embedded into the page template.
func CSS() string { return minifiedCSS }
// PageData represents the inputs to the template.
type PageData struct {
State string
IDPName string
HasAlertError bool
AlertMessage string
MinifiedCSS template.CSS
PostPath string
}

View File

@ -0,0 +1,68 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package loginhtml
import (
"bytes"
"fmt"
"testing"
"go.pinniped.dev/internal/testutil"
"github.com/stretchr/testify/require"
)
var (
testExpectedCSS = `html{height:100%}body{font-family:metropolis-light,Helvetica,sans-serif;display:flex;flex-flow:column wrap;justify-content:flex-start;align-items:center;background:linear-gradient(to top,#f8f8f8,white);min-height:100%}h1{font-size:20px;margin:0}.box{display:flex;flex-direction:column;flex-wrap:nowrap;border-radius:4px;border-color:#ddd;border-width:1px;border-style:solid;width:400px;padding:30px 30px 0;margin:60px 20px 0;background:#fff;font-size:14px}input{color:inherit;font:inherit;border:0;margin:0;outline:0;padding:0}.form-field{display:flex;margin-bottom:30px}.form-field input[type=password],.form-field input[type=text],.form-field input[type=submit]{width:100%;padding:1em}.form-field input[type=password],.form-field input[type=text]{border-radius:3px;border-width:1px;border-style:solid;border-color:#a6a6a6}.form-field input[type=submit]{background-color:#218fcf;color:#eee;font-weight:700;cursor:pointer;transition:all .3s}.form-field input[type=submit]:focus,.form-field input[type=submit]:hover{background-color:#1abfd3}.form-field input[type=submit]:active{transform:scale(.99)}.hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.alert{color:crimson}`
// It's okay if this changes in the future, but this gives us a chance to eyeball the formatting.
// Our browser-based integration tests should find any incompatibilities.
testExpectedCSP = `default-src 'none'; ` +
`style-src 'sha256-QC9ckaUFAdcN0Ysmu8q8iqCazYFgrJSQDJPa/przPXU='; ` +
`frame-ancestors 'none'`
)
func TestTemplate(t *testing.T) {
const (
testUpstreamName = "test-idp-name"
testPath = "test-post-path"
testEncodedState = "test-encoded-state"
testAlert = "test-alert-message"
)
var buf bytes.Buffer
pageInputs := &PageData{
PostPath: testPath,
State: testEncodedState,
IDPName: testUpstreamName,
HasAlertError: true,
AlertMessage: testAlert,
}
// Render with an alert.
expectedHTMLWithAlert := testutil.ExpectedLoginPageHTML(testExpectedCSS, testUpstreamName, testPath, testEncodedState, testAlert)
require.NoError(t, Template().Execute(&buf, pageInputs))
// t.Logf("actual value:\n%s", buf.String()) // useful when updating minify library causes new output
require.Equal(t, expectedHTMLWithAlert, buf.String())
// Render again without an alert.
pageInputs.HasAlertError = false
expectedHTMLWithoutAlert := testutil.ExpectedLoginPageHTML(testExpectedCSS, testUpstreamName, testPath, testEncodedState, "")
buf = bytes.Buffer{} // clear previous result from buffer
require.NoError(t, Template().Execute(&buf, pageInputs))
require.Equal(t, expectedHTMLWithoutAlert, buf.String())
}
func TestContentSecurityPolicy(t *testing.T) {
require.Equal(t, testExpectedCSP, ContentSecurityPolicy())
}
func TestCSS(t *testing.T) {
require.Equal(t, testExpectedCSS, CSS())
}
func TestHelpers(t *testing.T) {
require.Equal(t, "test", panicOnError("test", nil))
require.PanicsWithError(t, "some error", func() { panicOnError("", fmt.Errorf("some error")) })
}

View File

@ -0,0 +1,88 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"net/http"
"net/url"
"github.com/ory/fosite"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/downstreamsession"
"go.pinniped.dev/internal/plog"
)
func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error {
// Note that the login handler prevents this handler from being called with OIDC upstreams.
_, ldapUpstream, idpType, err := oidc.FindUpstreamIDPByNameAndType(upstreamIDPs, decodedState.UpstreamName, decodedState.UpstreamType)
if err != nil {
// This shouldn't normally happen because the authorization endpoint ensured that this provider existed
// at that time. It would be possible in the unlikely event that the provider was deleted during the login.
plog.Error("error finding upstream provider", err)
return httperr.Wrap(http.StatusUnprocessableEntity, "error finding upstream provider", err)
}
// Get the original params that were used at the authorization endpoint.
downstreamAuthParams, err := url.ParseQuery(decodedState.AuthParams)
if err != nil {
// This shouldn't really happen because the authorization endpoint encoded these query params correctly.
plog.Error("error reading state downstream auth params", err)
return httperr.New(http.StatusBadRequest, "error reading state downstream auth params")
}
// Recreate enough of the original authorize request so we can pass it to NewAuthorizeRequest().
reconstitutedAuthRequest := &http.Request{Form: downstreamAuthParams}
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), reconstitutedAuthRequest)
if err != nil {
// This shouldn't really happen because the authorization endpoint has already validated these params
// by calling NewAuthorizeRequest() itself.
plog.Error("error using state downstream auth params", err)
return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
}
// Automatically grant the openid, offline_access, and pinniped:request-audience scopes, but only if they were requested.
downstreamsession.GrantScopesIfRequested(authorizeRequester)
// Get the username and password form params from the POST body.
username := r.PostFormValue(usernameParamName)
password := r.PostFormValue(passwordParamName)
// Treat blank username or password as a bad username/password combination, as opposed to an internal error.
if username == "" || password == "" {
// User forgot to enter one of the required fields.
// The user may try to log in again if they'd like, so redirect back to the login page with an error.
return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr)
}
// Attempt to authenticate the user with the upstream IDP.
authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password)
if err != nil {
plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName())
// There was some problem during authentication with the upstream, aside from bad username/password.
// The user may try to log in again if they'd like, so redirect back to the login page with an error.
return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowInternalError)
}
if !authenticated {
// The upstream did not accept the username/password combination.
// The user may try to log in again if they'd like, so redirect back to the login page with an error.
return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr)
}
// We had previously interrupted the regular steps of the OIDC authcode flow to show the login page UI.
// Now the upstream IDP has authenticated the user, so now we're back into the regular OIDC authcode flow steps.
// Both success and error responses from this point onwards should look like the usual fosite redirect
// responses, and a happy redirect response will include a downstream authcode.
subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups()
customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse)
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false)
return nil
}
}

View File

@ -0,0 +1,748 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/fake"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil"
)
func TestPostLoginEndpoint(t *testing.T) {
const (
htmlContentType = "text/html; charset=utf-8"
happyDownstreamCSRF = "test-csrf"
happyDownstreamPKCE = "test-pkce"
happyDownstreamNonce = "test-nonce"
happyDownstreamStateVersion = "2"
happyEncodedUpstreamState = "fake-encoded-state-param-value"
downstreamIssuer = "https://my-downstream-issuer.com/path"
downstreamRedirectURI = "http://127.0.0.1/callback"
downstreamClientID = "pinniped-cli"
happyDownstreamState = "8b-state"
downstreamNonce = "some-nonce-value"
downstreamPKCEChallenge = "some-challenge"
downstreamPKCEChallengeMethod = "S256"
ldapUpstreamName = "some-ldap-idp"
ldapUpstreamType = "ldap"
ldapUpstreamResourceUID = "ldap-resource-uid"
activeDirectoryUpstreamName = "some-active-directory-idp"
activeDirectoryUpstreamType = "activedirectory"
activeDirectoryUpstreamResourceUID = "active-directory-resource-uid"
upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev"
userParam = "username"
passParam = "password"
badUserPassErrParamValue = "login_error"
internalErrParamValue = "internal_error"
)
var (
fositeMissingCodeChallengeErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must include a code_challenge when performing the authorize code flow, but it is missing.",
"state": happyDownstreamState,
}
fositeInvalidCodeChallengeErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The code_challenge_method is not supported, use S256 instead.",
"state": happyDownstreamState,
}
fositeMissingCodeChallengeMethodErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Clients must use code_challenge_method=S256, plain is not allowed.",
"state": happyDownstreamState,
}
fositePromptHasNoneAndOtherValueErrorQuery = map[string]string{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Parameter 'prompt' was set to 'none', but contains other values as well which is not allowed.",
"state": happyDownstreamState,
}
)
happyDownstreamScopesRequested := []string{"openid"}
happyDownstreamScopesGranted := []string{"openid"}
happyDownstreamRequestParamsQuery := url.Values{
"response_type": []string{"code"},
"scope": []string{strings.Join(happyDownstreamScopesRequested, " ")},
"client_id": []string{downstreamClientID},
"state": []string{happyDownstreamState},
"nonce": []string{downstreamNonce},
"code_challenge": []string{downstreamPKCEChallenge},
"code_challenge_method": []string{downstreamPKCEChallengeMethod},
"redirect_uri": []string{downstreamRedirectURI},
}
happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode()
copyOfHappyDownstreamRequestParamsQuery := func() url.Values {
params := url.Values{}
for k, v := range happyDownstreamRequestParamsQuery {
params[k] = make([]string, len(v))
copy(params[k], v)
}
return params
}
happyLDAPDecodedState := &oidc.UpstreamStateParamData{
AuthParams: happyDownstreamRequestParams,
UpstreamName: ldapUpstreamName,
UpstreamType: ldapUpstreamType,
Nonce: happyDownstreamNonce,
CSRFToken: happyDownstreamCSRF,
PKCECode: happyDownstreamPKCE,
FormatVersion: happyDownstreamStateVersion,
}
modifyHappyLDAPDecodedState := func(edit func(*oidc.UpstreamStateParamData)) *oidc.UpstreamStateParamData {
copyOfHappyLDAPDecodedState := *happyLDAPDecodedState
edit(&copyOfHappyLDAPDecodedState)
return &copyOfHappyLDAPDecodedState
}
happyActiveDirectoryDecodedState := &oidc.UpstreamStateParamData{
AuthParams: happyDownstreamRequestParams,
UpstreamName: activeDirectoryUpstreamName,
UpstreamType: activeDirectoryUpstreamType,
Nonce: happyDownstreamNonce,
CSRFToken: happyDownstreamCSRF,
PKCECode: happyDownstreamPKCE,
FormatVersion: happyDownstreamStateVersion,
}
happyLDAPUsername := "some-ldap-user"
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
happyLDAPPassword := "some-ldap-password" //nolint:gosec
happyLDAPUID := "some-ldap-uid"
happyLDAPUserDN := "cn=foo,dn=bar"
happyLDAPGroups := []string{"group1", "group2", "group3"}
happyLDAPExtraRefreshAttribute := "some-refresh-attribute"
happyLDAPExtraRefreshValue := "some-refresh-attribute-value"
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
require.NoError(t, err)
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
if username == "" || password == "" {
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
}
if username == happyLDAPUsername && password == happyLDAPPassword {
return &authenticators.Response{
User: &user.DefaultInfo{
Name: happyLDAPUsernameFromAuthenticator,
UID: happyLDAPUID,
Groups: happyLDAPGroups,
},
DN: happyLDAPUserDN,
ExtraRefreshAttributes: map[string]string{
happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue,
},
}, true, nil
}
return nil, false, nil
}
upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: parsedUpstreamLDAPURL,
AuthenticateFunc: ldapAuthenticateFunc,
}
upstreamActiveDirectoryIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: activeDirectoryUpstreamResourceUID,
URL: parsedUpstreamLDAPURL,
AuthenticateFunc: ldapAuthenticateFunc,
}
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
return nil, false, fmt.Errorf("some ldap upstream auth error")
},
}
expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName,
ProviderType: psession.ProviderTypeActiveDirectory,
OIDC: nil,
LDAP: nil,
ActiveDirectory: &psession.ActiveDirectorySessionData{
UserDN: happyLDAPUserDN,
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
},
}
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: psession.ProviderTypeLDAP,
OIDC: nil,
LDAP: &psession.LDAPSessionData{
UserDN: happyLDAPUserDN,
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
},
ActiveDirectory: nil,
}
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState
happyUsernamePasswordFormParams := url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{happyLDAPPassword}}
encodeQuery := func(query map[string]string) string {
values := url.Values{}
for k, v := range query {
values[k] = []string{v}
}
return values.Encode()
}
urlWithQuery := func(baseURL string, query map[string]string) string {
urlToReturn := fmt.Sprintf("%s?%s", baseURL, encodeQuery(query))
_, err := url.Parse(urlToReturn)
require.NoError(t, err, "urlWithQuery helper was used to create an illegal URL")
return urlToReturn
}
tests := []struct {
name string
idps *oidctestutil.UpstreamIDPListerBuilder
decodedState *oidc.UpstreamStateParamData
formParams url.Values
reqURIQuery url.Values
wantStatus int
wantContentType string
wantBodyString string
wantErr string
// Assertion that the response should be a redirect to the login page with an error param.
wantRedirectToLoginPageError string
// Assertions for when an authcode should be returned, i.e. the request was authenticated by an
// upstream LDAP or AD provider.
wantRedirectLocationRegexp string // for loose matching
wantRedirectLocationString string // for exact matching instead
wantBodyFormResponseRegexp string // for form_post html page matching instead
wantDownstreamRedirectURI string
wantDownstreamGrantedScopes []string
wantDownstreamIDTokenSubject string
wantDownstreamIDTokenUsername string
wantDownstreamIDTokenGroups []string
wantDownstreamRequestedScopes []string
wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string
wantDownstreamNonce string
wantDownstreamCustomSessionData *psession.CustomSessionData
// Authorization requests for either a successful OIDC upstream or for an error with any upstream
// should never use Kube storage. There is only one exception to this rule, which is that certain
// OIDC validations are checked in fosite after the OAuth authcode (and sometimes the OIDC session)
// is stored, so it is possible with an LDAP upstream to store objects and then return an error to
// the client anyway (which makes the stored objects useless, but oh well).
wantUnnecessaryStoredRecords int
}{
{
name: "happy LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy AD login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().
WithLDAP(&erroringUpstreamLDAPIdentityProvider).
WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // should pick this one
decodedState: happyActiveDirectoryDecodedState,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
},
{
name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["response_mode"] = []string{"form_post"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusOK,
wantContentType: htmlContentType,
wantBodyFormResponseRegexp: `(?s)<html.*<script>.*To finish logging in, paste this authorization code` +
`.*<form>.*<code id="manual-auth-code">(.+)</code>.*</html>`, // "(?s)" means match "." across newlines
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["redirect_uri"] = []string{"http://127.0.0.1:4242/callback"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback",
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy LDAP login when there are additional allowed downstream requested scopes",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["scope"] = []string{"openid offline_access pinniped:request-audience"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["scope"] = []string{"email"}
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
query["prompt"] = []string{"none login"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState, // no scopes granted
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: []string{"email"}, // only email was requested
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{}, // no scopes granted
wantDownstreamNonce: downstreamNonce,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "bad username LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{"wrong!"}, passParam: []string{happyLDAPPassword}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "bad password LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{"wrong!"}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "blank username LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{""}, passParam: []string{happyLDAPPassword}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "blank password LDAP login",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{""}},
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "username and password sent as URI query params should be ignored since they are expected in form post body",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
reqURIQuery: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: badUserPassErrParamValue,
},
{
name: "error during upstream LDAP authentication",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider),
decodedState: happyLDAPDecodedState,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectToLoginPageError: internalErrParamValue,
},
{
name: "downstream redirect uri does not match what is configured for client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["redirect_uri"] = []string{"http://127.0.0.1/wrong_callback"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream client does not exist",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["client_id"] = []string{"wrong_client_id"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream client is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "client_id")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "response type is unsupported",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["response_type"] = []string{"unsupported"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "response type is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "response_type")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "PKCE code_challenge is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "code_challenge")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "PKCE code_challenge_method is invalid",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["code_challenge_method"] = []string{"this-is-not-a-valid-pkce-alg"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "PKCE code_challenge_method is `plain`",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["code_challenge_method"] = []string{"plain"} // plain is not allowed
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "PKCE code_challenge_method is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
delete(query, "code_challenge_method")
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "prompt param is not allowed to have none and another legal value at the same time",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["prompt"] = []string{"none login"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error
},
{
name: "downstream state does not have enough entropy",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["state"] = []string{"short"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream scopes do not match what is configured for client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery()
query["scope"] = []string{"openid offline_access pinniped:request-audience scope_not_allowed"}
data.AuthParams = query.Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "no upstream providers are configured or provider cannot be found by name",
idps: oidctestutil.NewUpstreamIDPListerBuilder(), // empty
decodedState: happyLDAPDecodedState,
formParams: happyUsernamePasswordFormParams,
wantErr: "error finding upstream provider: provider not found",
},
{
name: "upstream provider cannot be found by name and type",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: happyActiveDirectoryDecodedState, // correct upstream IDP name, but wrong upstream IDP type
formParams: happyUsernamePasswordFormParams,
wantErr: "error finding upstream provider: provider not found",
},
}
for _, test := range tests {
tt := test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
kubeClient := fake.NewSimpleClientset()
secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
// Configure fosite the same way that the production code would.
// Inject this into our test subject at the last second so we get a fresh storage for every test.
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
kubeOauthStore := oidc.NewKubeStorage(secretsClient, timeoutsConfiguration)
hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") }
require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes")
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
oauthHelper := oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration)
req := httptest.NewRequest(http.MethodPost, "/ignored", strings.NewReader(tt.formParams.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if tt.reqURIQuery != nil {
req.URL.RawQuery = tt.reqURIQuery.Encode()
}
rsp := httptest.NewRecorder()
subject := NewPostHandler(downstreamIssuer, tt.idps.Build(), oauthHelper)
err := subject(rsp, req, happyEncodedUpstreamState, tt.decodedState)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
require.Empty(t, kubeClient.Actions())
return // the http response doesn't matter when the function returns an error, because the caller should handle the error
}
// Otherwise, expect no error.
require.NoError(t, err)
require.Equal(t, tt.wantStatus, rsp.Code)
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), tt.wantContentType)
actualLocation := rsp.Header().Get("Location")
switch {
case tt.wantRedirectLocationRegexp != "":
// Expecting a success redirect to the client.
require.Equal(t, tt.wantBodyString, rsp.Body.String())
require.Len(t, rsp.Header().Values("Location"), 1)
oidctestutil.RequireAuthCodeRegexpMatch(
t,
actualLocation,
tt.wantRedirectLocationRegexp,
kubeClient,
secretsClient,
kubeOauthStore,
tt.wantDownstreamGrantedScopes,
tt.wantDownstreamIDTokenSubject,
tt.wantDownstreamIDTokenUsername,
tt.wantDownstreamIDTokenGroups,
tt.wantDownstreamRequestedScopes,
tt.wantDownstreamPKCEChallenge,
tt.wantDownstreamPKCEChallengeMethod,
tt.wantDownstreamNonce,
downstreamClientID,
tt.wantDownstreamRedirectURI,
tt.wantDownstreamCustomSessionData,
)
case tt.wantRedirectToLoginPageError != "":
// Expecting an error redirect to the login UI page.
require.Equal(t, tt.wantBodyString, rsp.Body.String())
expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath +
"?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState
require.Equal(t, expectedLocation, actualLocation)
require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords)
case tt.wantRedirectLocationString != "":
// Expecting an error redirect to the client.
require.Equal(t, tt.wantBodyString, rsp.Body.String())
require.Equal(t, tt.wantRedirectLocationString, actualLocation)
require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords)
case tt.wantBodyFormResponseRegexp != "":
// Expecting the body of the response to be a html page with a form (for "response_mode=form_post").
_, hasLocationHeader := rsp.Header()["Location"]
require.False(t, hasLocationHeader)
oidctestutil.RequireAuthCodeRegexpMatch(
t,
rsp.Body.String(),
tt.wantBodyFormResponseRegexp,
kubeClient,
secretsClient,
kubeOauthStore,
tt.wantDownstreamGrantedScopes,
tt.wantDownstreamIDTokenSubject,
tt.wantDownstreamIDTokenUsername,
tt.wantDownstreamIDTokenGroups,
tt.wantDownstreamRequestedScopes,
tt.wantDownstreamPKCEChallenge,
tt.wantDownstreamPKCEChallengeMethod,
tt.wantDownstreamNonce,
downstreamClientID,
tt.wantDownstreamRedirectURI,
tt.wantDownstreamCustomSessionData,
)
default:
require.Failf(t, "test should have expected a redirect or form body",
"actual location was %q", actualLocation)
}
})
}
}

View File

@ -1,20 +1,30 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// Package oidc contains common OIDC functionality needed by Pinniped. // Package oidc contains common OIDC functionality needed by Pinniped.
package oidc package oidc
import ( import (
"crypto/subtle"
"errors"
"fmt"
"net/http"
"time" "time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc" coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/felixge/httpsnoop"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/fosite/compose" "github.com/ory/fosite/compose"
errorsx "github.com/pkg/errors"
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/csrftoken"
"go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/oidc/provider/formposthtml" "go.pinniped.dev/internal/oidc/provider/formposthtml"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/pkce" "go.pinniped.dev/pkg/oidcclient/pkce"
) )
@ -26,13 +36,17 @@ const (
CallbackEndpointPath = "/callback" CallbackEndpointPath = "/callback"
JWKSEndpointPath = "/jwks.json" JWKSEndpointPath = "/jwks.json"
PinnipedIDPsPathV1Alpha1 = "/v1alpha1/pinniped_identity_providers" PinnipedIDPsPathV1Alpha1 = "/v1alpha1/pinniped_identity_providers"
PinnipedLoginPath = "/login"
) )
const ( const (
// Just in case we need to make a breaking change to the format of the upstream state param, // Just in case we need to make a breaking change to the format of the upstream state param,
// we are including a format version number. This gives the opportunity for a future version of Pinniped // we are including a format version number. This gives the opportunity for a future version of Pinniped
// to have the consumer of this format decide to reject versions that it doesn't understand. // to have the consumer of this format decide to reject versions that it doesn't understand.
UpstreamStateParamFormatVersion = "1" //
// Version 1 was the original version.
// Version 2 added the UpstreamType field to the UpstreamStateParamData struct.
UpstreamStateParamFormatVersion = "2"
// The `name` passed to the encoder for encoding the upstream state param value. This name is short // The `name` passed to the encoder for encoding the upstream state param value. This name is short
// because it will be encoded into the upstream state param value and we're trying to keep that small. // because it will be encoded into the upstream state param value and we're trying to keep that small.
@ -93,6 +107,7 @@ type Codec interface {
type UpstreamStateParamData struct { type UpstreamStateParamData struct {
AuthParams string `json:"p"` AuthParams string `json:"p"`
UpstreamName string `json:"u"` UpstreamName string `json:"u"`
UpstreamType string `json:"t"`
Nonce nonce.Nonce `json:"n"` Nonce nonce.Nonce `json:"n"`
CSRFToken csrftoken.CSRFToken `json:"c"` CSRFToken csrftoken.CSRFToken `json:"c"`
PKCECode pkce.Code `json:"k"` PKCECode pkce.Code `json:"k"`
@ -295,3 +310,171 @@ func ScopeWasRequested(authorizeRequester fosite.AuthorizeRequester, scopeName s
} }
return false return false
} }
func ReadStateParamAndValidateCSRFCookie(r *http.Request, cookieDecoder Decoder, stateDecoder Decoder) (string, *UpstreamStateParamData, error) {
csrfValue, err := readCSRFCookie(r, cookieDecoder)
if err != nil {
return "", nil, err
}
encodedState, decodedState, err := readStateParam(r, stateDecoder)
if err != nil {
return "", nil, err
}
err = validateCSRFValue(decodedState, csrfValue)
if err != nil {
return "", nil, err
}
return encodedState, decodedState, nil
}
func readCSRFCookie(r *http.Request, cookieDecoder Decoder) (csrftoken.CSRFToken, error) {
receivedCSRFCookie, err := r.Cookie(CSRFCookieName)
if err != nil {
// Error means that the cookie was not found
return "", httperr.Wrap(http.StatusForbidden, "CSRF cookie is missing", err)
}
var csrfFromCookie csrftoken.CSRFToken
err = cookieDecoder.Decode(CSRFCookieEncodingName, receivedCSRFCookie.Value, &csrfFromCookie)
if err != nil {
return "", httperr.Wrap(http.StatusForbidden, "error reading CSRF cookie", err)
}
return csrfFromCookie, nil
}
func readStateParam(r *http.Request, stateDecoder Decoder) (string, *UpstreamStateParamData, error) {
encodedState := r.FormValue("state")
if encodedState == "" {
return "", nil, httperr.New(http.StatusBadRequest, "state param not found")
}
var state UpstreamStateParamData
if err := stateDecoder.Decode(
UpstreamStateParamEncodingName,
r.FormValue("state"),
&state,
); err != nil {
return "", nil, httperr.New(http.StatusBadRequest, "error reading state")
}
if state.FormatVersion != UpstreamStateParamFormatVersion {
return "", nil, httperr.New(http.StatusUnprocessableEntity, "state format version is invalid")
}
return encodedState, &state, nil
}
func validateCSRFValue(state *UpstreamStateParamData, csrfCookieValue csrftoken.CSRFToken) error {
if subtle.ConstantTimeCompare([]byte(state.CSRFToken), []byte(csrfCookieValue)) != 1 {
return httperr.New(http.StatusForbidden, "CSRF value does not match")
}
return nil
}
// FindUpstreamIDPByNameAndType finds the requested IDP by name and type, or returns an error.
// Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values.
func FindUpstreamIDPByNameAndType(
idpLister UpstreamIdentityProvidersLister,
upstreamName string,
upstreamType string,
) (
provider.UpstreamOIDCIdentityProviderI,
provider.UpstreamLDAPIdentityProviderI,
psession.ProviderType,
error,
) {
switch upstreamType {
case string(v1alpha1.IDPTypeOIDC):
for _, p := range idpLister.GetOIDCIdentityProviders() {
if p.GetName() == upstreamName {
return p, nil, psession.ProviderTypeOIDC, nil
}
}
case string(v1alpha1.IDPTypeLDAP):
for _, p := range idpLister.GetLDAPIdentityProviders() {
if p.GetName() == upstreamName {
return nil, p, psession.ProviderTypeLDAP, nil
}
}
case string(v1alpha1.IDPTypeActiveDirectory):
for _, p := range idpLister.GetActiveDirectoryIdentityProviders() {
if p.GetName() == upstreamName {
return nil, p, psession.ProviderTypeActiveDirectory, nil
}
}
}
return nil, nil, "", errors.New("provider not found")
}
// WriteAuthorizeError writes an authorization error as it should be returned by the authorization endpoint and other
// similar endpoints that are the end of the downstream authcode flow. Errors responses are written in the usual fosite style.
func WriteAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) {
if plog.Enabled(plog.LevelTrace) {
// When trace level logging is enabled, include the stack trace in the log message.
keysAndValues := FositeErrorForLog(err)
errWithStack := errorsx.WithStack(err)
keysAndValues = append(keysAndValues, "errWithStack")
// klog always prints error values using %s, which does not include stack traces,
// so convert the error to a string which includes the stack trace here.
keysAndValues = append(keysAndValues, fmt.Sprintf("%+v", errWithStack))
plog.Trace("authorize response error", keysAndValues...)
} else {
plog.Info("authorize response error", FositeErrorForLog(err)...)
}
if isBrowserless {
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
}
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
}
// PerformAuthcodeRedirect successfully completes a downstream login by creating a session and
// writing the authcode redirect response as it should be returned by the authorization endpoint and other
// similar endpoints that are the end of the downstream authcode flow.
func PerformAuthcodeRedirect(
r *http.Request,
w http.ResponseWriter,
oauthHelper fosite.OAuth2Provider,
authorizeRequester fosite.AuthorizeRequester,
openIDSession *psession.PinnipedSession,
isBrowserless bool,
) {
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil {
plog.WarningErr("error while generating and saving authcode", err)
WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless)
return
}
if isBrowserless {
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
}
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
}
func rewriteStatusSeeOtherToStatusFoundForBrowserless(w http.ResponseWriter) http.ResponseWriter {
// rewrite http.StatusSeeOther to http.StatusFound for backwards compatibility with old pinniped CLIs.
// we can drop this in a few releases once we feel enough time has passed for users to update.
//
// WriteAuthorizeResponse/WriteAuthorizeError calls used to result in http.StatusFound until
// https://github.com/ory/fosite/pull/636 changed it to http.StatusSeeOther to address
// https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
// Safari has the bad behavior in the case of http.StatusFound and not just http.StatusTemporaryRedirect.
//
// in the browserless flows, the OAuth client is the pinniped CLI and it already has access to the user's
// password. Thus there is no security issue with using http.StatusFound vs. http.StatusSeeOther.
return httpsnoop.Wrap(w, httpsnoop.Hooks{
WriteHeader: func(delegate httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
return func(code int) {
if code == http.StatusSeeOther {
code = http.StatusFound
}
delegate(code)
}
},
})
}

View File

@ -0,0 +1,15 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package csp defines helpers related to HTML Content Security Policies.
package csp
import (
"crypto/sha256"
"encoding/base64"
)
func Hash(s string) string {
hashBytes := sha256.Sum256([]byte(s))
return "sha256-" + base64.StdEncoding.EncodeToString(hashBytes[:])
}

View File

@ -0,0 +1,15 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package csp
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestHash(t *testing.T) {
// Example test vector from https://content-security-policy.com/hash/.
require.Equal(t, "sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=", Hash("doSomething();"))
}

View File

@ -6,23 +6,23 @@
package formposthtml package formposthtml
import ( import (
"crypto/sha256"
_ "embed" // Needed to trigger //go:embed directives below. _ "embed" // Needed to trigger //go:embed directives below.
"encoding/base64"
"html/template" "html/template"
"strings" "strings"
"github.com/tdewolff/minify/v2/minify" "github.com/tdewolff/minify/v2/minify"
"go.pinniped.dev/internal/oidc/provider/csp"
) )
var ( var (
//go:embed form_post.css //go:embed form_post.css
rawCSS string rawCSS string
minifiedCSS = mustMinify(minify.CSS(rawCSS)) minifiedCSS = panicOnError(minify.CSS(rawCSS))
//go:embed form_post.js //go:embed form_post.js
rawJS string rawJS string
minifiedJS = mustMinify(minify.JS(rawJS)) minifiedJS = panicOnError(minify.JS(rawJS))
//go:embed form_post.gohtml //go:embed form_post.gohtml
rawHTMLTemplate string rawHTMLTemplate string
@ -37,28 +37,23 @@ var parsedHTMLTemplate = template.Must(template.New("form_post.gohtml").Funcs(te
// Generate the CSP header value once since it's effectively constant. // Generate the CSP header value once since it's effectively constant.
var cspValue = strings.Join([]string{ var cspValue = strings.Join([]string{
`default-src 'none'`, `default-src 'none'`,
`script-src '` + cspHash(minifiedJS) + `'`, `script-src '` + csp.Hash(minifiedJS) + `'`,
`style-src '` + cspHash(minifiedCSS) + `'`, `style-src '` + csp.Hash(minifiedCSS) + `'`,
`img-src data:`, `img-src data:`,
`connect-src *`, `connect-src *`,
`frame-ancestors 'none'`, `frame-ancestors 'none'`,
}, "; ") }, "; ")
func mustMinify(s string, err error) string { func panicOnError(s string, err error) string {
if err != nil { if err != nil {
panic(err) panic(err)
} }
return s return s
} }
func cspHash(s string) string {
hashBytes := sha256.Sum256([]byte(s))
return "sha256-" + base64.StdEncoding.EncodeToString(hashBytes[:])
}
// ContentSecurityPolicy returns the Content-Security-Policy header value to make the Template() operate correctly. // ContentSecurityPolicy returns the Content-Security-Policy header value to make the Template() operate correctly.
// //
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src#:~:text=%27%3Chash-algorithm%3E-%3Cbase64-value%3E%27. // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy.
func ContentSecurityPolicy() string { return cspValue } func ContentSecurityPolicy() string { return cspValue }
// Template returns the html/template.Template for rendering the response_type=form_post response page. // Template returns the html/template.Template for rendering the response_type=form_post response page.

View File

@ -93,10 +93,6 @@ func TestContentSecurityPolicyHashes(t *testing.T) {
} }
func TestHelpers(t *testing.T) { func TestHelpers(t *testing.T) {
// These are silly tests but it's easy to we might as well have them. require.Equal(t, "test", panicOnError("test", nil))
require.Equal(t, "test", mustMinify("test", nil)) require.PanicsWithError(t, "some error", func() { panicOnError("", fmt.Errorf("some error")) })
require.PanicsWithError(t, "some error", func() { mustMinify("", fmt.Errorf("some error")) })
// Example test vector from https://content-security-policy.com/hash/.
require.Equal(t, "sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=", cspHash("doSomething();"))
} }

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package manager package manager
@ -18,6 +18,7 @@ import (
"go.pinniped.dev/internal/oidc/dynamiccodec" "go.pinniped.dev/internal/oidc/dynamiccodec"
"go.pinniped.dev/internal/oidc/idpdiscovery" "go.pinniped.dev/internal/oidc/idpdiscovery"
"go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/oidc/login"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/oidc/token" "go.pinniped.dev/internal/oidc/token"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
@ -134,6 +135,13 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs
oauthHelperWithKubeStorage, oauthHelperWithKubeStorage,
) )
m.providerHandlers[(issuerHostWithPath + oidc.PinnipedLoginPath)] = login.NewHandler(
upstreamStateEncoder,
csrfCookieEncoder,
login.NewGetHandler(incomingProvider.IssuerPath()+oidc.PinnipedLoginPath),
login.NewPostHandler(issuer, m.upstreamIDPs, oauthHelperWithKubeStorage),
)
plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer) plog.Debug("oidc provider manager added or updated issuer", "issuer", issuer)
} }
} }

107
internal/plog/config.go Normal file
View File

@ -0,0 +1,107 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"context"
"encoding/json"
"strconv"
"time"
"go.uber.org/zap/zapcore"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/component-base/logs"
"go.pinniped.dev/internal/constable"
)
type LogFormat string
func (l *LogFormat) UnmarshalJSON(b []byte) error {
switch string(b) {
case `""`, `"json"`:
*l = FormatJSON
case `"text"`:
*l = FormatText
// there is no "cli" case because it is not a supported option via our config
default:
return errInvalidLogFormat
}
return nil
}
const (
FormatJSON LogFormat = "json"
FormatText LogFormat = "text"
FormatCLI LogFormat = "cli" // only used by the pinniped CLI and not the server components
errInvalidLogLevel = constable.Error("invalid log level, valid choices are the empty string, info, debug, trace and all")
errInvalidLogFormat = constable.Error("invalid log format, valid choices are the empty string, json and text")
)
var _ json.Unmarshaler = func() *LogFormat {
var f LogFormat
return &f
}()
type LogSpec struct {
Level LogLevel `json:"level,omitempty"`
Format LogFormat `json:"format,omitempty"`
}
func MaybeSetDeprecatedLogLevel(level *LogLevel, log *LogSpec) {
if level != nil {
Warning("logLevel is deprecated, set log.level instead")
log.Level = *level
}
}
func ValidateAndSetLogLevelAndFormatGlobally(ctx context.Context, spec LogSpec) error {
klogLevel := klogLevelForPlogLevel(spec.Level)
if klogLevel < 0 {
return errInvalidLogLevel
}
// set the global log levels used by our code and the kube code underneath us
if _, err := logs.GlogSetter(strconv.Itoa(int(klogLevel))); err != nil {
panic(err) // programmer error
}
globalLevel.SetLevel(zapcore.Level(-klogLevel)) // klog levels are inverted when zap handles them
var encoding string
switch spec.Format {
case "", FormatJSON:
encoding = "json"
case FormatCLI:
encoding = "console"
case FormatText:
encoding = "text"
default:
return errInvalidLogFormat
}
log, flush, err := newLogr(ctx, encoding, klogLevel)
if err != nil {
return err
}
setGlobalLoggers(log, flush)
// nolint: exhaustive // the switch above is exhaustive for format already
switch spec.Format {
case FormatCLI:
return nil // do not spawn go routines on the CLI to allow the CLI to call this more than once
case FormatText:
Warning("setting log.format to 'text' is deprecated - this option will be removed in a future release")
}
// do spawn go routines on the server
go wait.UntilWithContext(ctx, func(_ context.Context) { flush() }, time.Minute)
go func() {
<-ctx.Done()
flush() // best effort flush before shutdown as this is not coordinated with a wait group
}()
return nil
}

View File

@ -0,0 +1,359 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"runtime"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
"k8s.io/klog/v2/textlogger"
clocktesting "k8s.io/utils/clock/testing"
)
func TestFormat(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
var buf bytes.Buffer
scanner := bufio.NewScanner(&buf)
now, err := time.Parse(time.RFC3339Nano, "2022-11-21T23:37:26.953313745Z")
require.NoError(t, err)
fakeClock := clocktesting.NewFakeClock(now)
nowStr := now.Local().Format(time.RFC1123)
ctx = TestZapOverrides(ctx, t, &buf, nil, zap.WithClock(ZapClock(fakeClock)))
err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug})
require.NoError(t, err)
wd, err := os.Getwd()
require.NoError(t, err)
const startLogLine = 46 // make this match the current line number
Info("hello", "happy", "day", "duration", time.Hour+time.Minute)
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.JSONEq(t, fmt.Sprintf(`
{
"level": "info",
"timestamp": "2022-11-21T23:37:26.953313Z",
"caller": "%s/config_test.go:%d$plog.TestFormat",
"message": "hello",
"happy": "day",
"duration": "1h1m0s"
}`, wd, startLogLine+2), scanner.Text())
Logr().WithName("burrito").Error(errInvalidLogLevel, "wee", "a", "b")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.JSONEq(t, fmt.Sprintf(`
{
"level": "error",
"timestamp": "2022-11-21T23:37:26.953313Z",
"caller": "%s/config_test.go:%d$plog.TestFormat",
"message": "wee",
"a": "b",
"error": "invalid log level, valid choices are the empty string, info, debug, trace and all",
"logger": "burrito"
}`, wd, startLogLine+2+13), scanner.Text())
Logr().V(klogLevelWarning).Info("hey") // note that this fails to set the custom warning key because it is not via plog
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.JSONEq(t, fmt.Sprintf(`
{
"level": "info",
"timestamp": "2022-11-21T23:37:26.953313Z",
"caller": "%s/config_test.go:%d$plog.TestFormat",
"message": "hey"
}`, wd, startLogLine+2+13+14), scanner.Text())
Warning("bad stuff") // note that this sets the custom warning key because it is via plog
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.JSONEq(t, fmt.Sprintf(`
{
"level": "info",
"timestamp": "2022-11-21T23:37:26.953313Z",
"caller": "%s/config_test.go:%d$plog.TestFormat",
"message": "bad stuff",
"warning": true
}`, wd, startLogLine+2+13+14+11), scanner.Text())
func() { DebugErr("something happened", errInvalidLogFormat, "an", "item") }()
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.JSONEq(t, fmt.Sprintf(`
{
"level": "debug",
"timestamp": "2022-11-21T23:37:26.953313Z",
"caller": "%s/config_test.go:%d$plog.TestFormat.func1",
"message": "something happened",
"error": "invalid log format, valid choices are the empty string, json and text",
"an": "item"
}`, wd, startLogLine+2+13+14+11+12), scanner.Text())
Trace("should not be logged", "for", "sure")
require.Empty(t, buf.String())
Logr().V(klogLevelAll).Info("also should not be logged", "open", "close")
require.Empty(t, buf.String())
ctx = TestZapOverrides(ctx, t, &buf, nil, zap.WithClock(ZapClock(fakeClock)), zap.AddStacktrace(LevelInfo))
err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug})
require.NoError(t, err)
WithName("stacky").WithName("does").Info("has a stack trace!")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.JSONEq(t, fmt.Sprintf(`
{
"level": "info",
"timestamp": "2022-11-21T23:37:26.953313Z",
"caller": "%s/config_test.go:%d$plog.TestFormat",
"message": "has a stack trace!",
"logger": "stacky.does",
"stacktrace": %s
}`, wd, startLogLine+2+13+14+11+12+24,
strconv.Quote(
fmt.Sprintf(
`go.pinniped.dev/internal/plog.TestFormat
%s/config_test.go:%d
testing.tRunner
%s/src/testing/testing.go:1439`,
wd, startLogLine+2+13+14+11+12+24, runtime.GOROOT(),
),
),
), scanner.Text())
ctx = TestZapOverrides(ctx, t, &buf, nil, zap.WithClock(ZapClock(fakeClock)))
err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug, Format: FormatCLI})
require.NoError(t, err)
DebugErr("something happened", errInvalidLogFormat, "an", "item")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(nowStr+` plog/config_test.go:%d something happened {"error": "invalid log format, valid choices are the empty string, json and text", "an": "item"}`,
startLogLine+2+13+14+11+12+24+28), scanner.Text())
Logr().WithName("burrito").Error(errInvalidLogLevel, "wee", "a", "b", "slightly less than a year", 363*24*time.Hour, "slightly more than 2 years", 2*367*24*time.Hour)
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(nowStr+` burrito plog/config_test.go:%d wee {"a": "b", "slightly less than a year": "363d", "slightly more than 2 years": "2y4d", "error": "invalid log level, valid choices are the empty string, info, debug, trace and all"}`,
startLogLine+2+13+14+11+12+24+28+6), scanner.Text())
origTimeNow := textlogger.TimeNow
t.Cleanup(func() {
textlogger.TimeNow = origTimeNow
})
textlogger.TimeNow = func() time.Time {
return now
}
old := New().WithName("created before mode change").WithValues("is", "old")
err = ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: LevelDebug, Format: FormatText})
require.NoError(t, err)
pid := os.Getpid()
// check for the deprecation warning
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config.go:96] "setting log.format to 'text' is deprecated - this option will be removed in a future release" warning=true`,
pid), scanner.Text())
Debug("what is happening", "does klog", "work?")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "what is happening" does klog="work?"`,
pid, startLogLine+2+13+14+11+12+24+28+6+26), scanner.Text())
Logr().WithName("panda").V(KlogLevelDebug).Info("are the best", "yes?", "yes.")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "panda: are the best" yes?="yes."`,
pid, startLogLine+2+13+14+11+12+24+28+6+26+6), scanner.Text())
New().WithName("hi").WithName("there").WithValues("a", 1, "b", 2).Always("do it")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "hi/there: do it" a=1 b=2`,
pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6), scanner.Text())
l := WithValues("x", 33, "z", 22)
l.Debug("what to do")
l.Debug("and why")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "what to do" x=33 z=22`,
pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7), scanner.Text())
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "and why" x=33 z=22`,
pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1), scanner.Text())
old.Always("should be klog text format", "for", "sure")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "created before mode change: should be klog text format" is="old" for="sure"`,
pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1+10), scanner.Text())
// make sure child loggers do not share state
old1 := old.WithValues("i am", "old1")
old2 := old.WithName("old2")
old1.Warning("warn")
old2.Info("info")
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "created before mode change: warn" is="old" i am="old1" warning=true`,
pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1+10+9), scanner.Text())
require.True(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Equal(t, fmt.Sprintf(`I1121 23:37:26.953313%8d config_test.go:%d] "created before mode change/old2: info" is="old"`,
pid, startLogLine+2+13+14+11+12+24+28+6+26+6+6+7+1+10+9+1), scanner.Text())
Trace("should not be logged", "for", "sure")
require.Empty(t, buf.String())
Logr().V(klogLevelAll).Info("also should not be logged", "open", "close")
require.Empty(t, buf.String())
require.False(t, scanner.Scan())
require.NoError(t, scanner.Err())
require.Empty(t, scanner.Text())
require.Empty(t, buf.String())
}
func TestValidateAndSetLogLevelGlobally(t *testing.T) {
originalLogLevel := getKlogLevel()
require.GreaterOrEqual(t, int(originalLogLevel), int(klog.Level(0)), "cannot get klog level")
tests := []struct {
name string
level LogLevel
wantLevel klog.Level
wantEnabled []LogLevel
wantErr string
}{
{
name: "unset",
wantLevel: 0,
wantEnabled: []LogLevel{LevelWarning},
},
{
name: "warning",
level: LevelWarning,
wantLevel: 0,
wantEnabled: []LogLevel{LevelWarning},
},
{
name: "info",
level: LevelInfo,
wantLevel: 2,
wantEnabled: []LogLevel{LevelWarning, LevelInfo},
},
{
name: "debug",
level: LevelDebug,
wantLevel: 4,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug},
},
{
name: "trace",
level: LevelTrace,
wantLevel: 6,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace},
},
{
name: "all",
level: LevelAll,
wantLevel: 108,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll},
},
{
name: "invalid level",
level: "panda",
wantLevel: originalLogLevel,
wantErr: errInvalidLogLevel.Error(),
},
}
for _, tt := range tests {
tt := tt // capture range variable
t.Run(tt.name, func(t *testing.T) {
defer func() {
undoGlobalLogLevelChanges(t, originalLogLevel)
}()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
err := ValidateAndSetLogLevelAndFormatGlobally(ctx, LogSpec{Level: tt.level})
require.Equal(t, tt.wantErr, errString(err))
require.Equal(t, tt.wantLevel, getKlogLevel())
if tt.wantEnabled != nil {
allLevels := []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll}
for _, level := range allLevels {
if contains(tt.wantEnabled, level) {
require.Truef(t, Enabled(level), "wanted %q to be enabled", level)
} else {
require.False(t, Enabled(level), "did not want %q to be enabled", level)
}
}
}
})
}
require.Equal(t, originalLogLevel, getKlogLevel())
}
func contains(haystack []LogLevel, needle LogLevel) bool {
for _, hay := range haystack {
if hay == needle {
return true
}
}
return false
}
func errString(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func undoGlobalLogLevelChanges(t *testing.T, originalLogLevel klog.Level) {
t.Helper()
_, err := logs.GlogSetter(strconv.Itoa(int(originalLogLevel)))
require.NoError(t, err)
}
func getKlogLevel() klog.Level {
// hack around klog not exposing a Get method
for i := klog.Level(0); i < 256; i++ {
if klog.V(i).Enabled() {
continue
}
return i - 1
}
return -1
}

77
internal/plog/global.go Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"context"
"fmt"
"net/url"
"sync"
"github.com/go-logr/logr"
"go.uber.org/zap"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
)
// nolint: gochecknoglobals
var (
// note that these globals have no locks on purpose - they are expected to be set at init and then again after config parsing.
globalLevel zap.AtomicLevel
globalLogger logr.Logger
globalFlush func()
// used as a temporary storage for a buffer per call of newLogr. see the init function below for more details.
sinkMap sync.Map
)
// nolint: gochecknoinits
func init() {
// make sure we always have a functional global logger
globalLevel = zap.NewAtomicLevelAt(0) // log at the 0 verbosity level to start with, i.e. the "always" logs
// use json encoding to start with
// the context here is just used for test injection and thus can be ignored
log, flush, err := newLogr(context.Background(), "json", 0)
if err != nil {
panic(err) // default logging config must always work
}
setGlobalLoggers(log, flush)
// this is a little crazy but zap's builder code does not allow us to directly specify what
// writer we want to use as our log sink. to get around this limitation in tests, we use a
// global map to temporarily hold the writer (the key is a random string that is generated
// per invocation of newLogr). we register a fake "pinniped" scheme so that we can lookup
// the writer via pinniped:///<per newLogr invocation random string>.
if err := zap.RegisterSink("pinniped", func(u *url.URL) (zap.Sink, error) {
value, ok := sinkMap.Load(u.Path)
if !ok {
return nil, fmt.Errorf("key %q not in global sink", u.Path)
}
return value.(zap.Sink), nil
}); err != nil {
panic(err) // custom sink must always work
}
}
// Deprecated: Use New instead. This is meant for old code only.
// New provides a more ergonomic API and correctly responds to global log config change.
func Logr() logr.Logger {
return globalLogger
}
func Setup() func() {
logs.InitLogs()
return func() {
logs.FlushLogs()
globalFlush()
}
}
// setGlobalLoggers sets the plog and klog global loggers. it is *not* go routine safe.
func setGlobalLoggers(log logr.Logger, flush func()) {
// a contextual logger does its own level based enablement checks, which is true for all of our loggers
klog.SetLoggerWithOptions(log, klog.ContextualLogger(true), klog.FlushLogger(flush))
globalLogger = log
globalFlush = flush
}

View File

@ -1,32 +0,0 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"fmt"
"k8s.io/klog/v2"
)
// KObj is (mostly) copied from klog - it is a standard way to represent a metav1.Object in logs.
func KObj(obj klog.KMetadata) string {
return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
}
func klogLevelForPlogLevel(plogLevel LogLevel) klog.Level {
switch plogLevel {
case LevelWarning:
return klogLevelWarning // unset means minimal logs (Error and Warning)
case LevelInfo:
return klogLevelInfo
case LevelDebug:
return klogLevelDebug
case LevelTrace:
return klogLevelTrace
case LevelAll:
return klogLevelAll + 100 // make all really mean all
default:
return -1
}
}

View File

@ -4,18 +4,18 @@
package plog package plog
import ( import (
"strconv" "go.uber.org/zap/zapcore"
"k8s.io/component-base/logs"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"go.pinniped.dev/internal/constable"
) )
// LogLevel is an enum that controls verbosity of logs. // LogLevel is an enum that controls verbosity of logs.
// Valid values in order of increasing verbosity are leaving it unset, info, debug, trace and all. // Valid values in order of increasing verbosity are leaving it unset, info, debug, trace and all.
type LogLevel string type LogLevel string
func (l LogLevel) Enabled(_ zapcore.Level) bool {
return Enabled(l) // this basically says "log if the global plog level is l or greater"
}
const ( const (
// LevelWarning (i.e. leaving the log level unset) maps to klog log level 0. // LevelWarning (i.e. leaving the log level unset) maps to klog log level 0.
LevelWarning LogLevel = "" LevelWarning LogLevel = ""
@ -27,33 +27,40 @@ const (
LevelTrace LogLevel = "trace" LevelTrace LogLevel = "trace"
// LevelAll maps to klog log level 100 (conceptually it is log level 8). // LevelAll maps to klog log level 100 (conceptually it is log level 8).
LevelAll LogLevel = "all" LevelAll LogLevel = "all"
errInvalidLogLevel = constable.Error("invalid log level, valid choices are the empty string, info, debug, trace and all")
) )
var _ zapcore.LevelEnabler = LevelWarning
const ( const (
klogLevelWarning = iota * 2 klogLevelWarning = iota * 2
klogLevelInfo KlogLevelInfo
klogLevelDebug KlogLevelDebug
klogLevelTrace KlogLevelTrace
klogLevelAll klogLevelAll
) )
func ValidateAndSetLogLevelGlobally(level LogLevel) error {
klogLevel := klogLevelForPlogLevel(level)
if klogLevel < 0 {
return errInvalidLogLevel
}
if _, err := logs.GlogSetter(strconv.Itoa(int(klogLevel))); err != nil {
panic(err) // programmer error
}
return nil
}
// Enabled returns whether the provided plog level is enabled, i.e., whether print statements at the // Enabled returns whether the provided plog level is enabled, i.e., whether print statements at the
// provided level will show up. // provided level will show up.
func Enabled(level LogLevel) bool { func Enabled(level LogLevel) bool {
return klog.V(klogLevelForPlogLevel(level)).Enabled() l := klogLevelForPlogLevel(level)
// check that both our global level and the klog global level agree that the plog level is enabled
// klog levels are inverted when zap handles them
return globalLevel.Enabled(zapcore.Level(-l)) && klog.V(l).Enabled()
}
func klogLevelForPlogLevel(plogLevel LogLevel) klog.Level {
switch plogLevel {
case LevelWarning:
return klogLevelWarning // unset means minimal logs (Error and Warning)
case LevelInfo:
return KlogLevelInfo
case LevelDebug:
return KlogLevelDebug
case LevelTrace:
return KlogLevelTrace
case LevelAll:
return klogLevelAll + 100 // make all really mean all
default:
return -1
}
} }

View File

@ -1,128 +0,0 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"strconv"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
)
func TestValidateAndSetLogLevelGlobally(t *testing.T) {
originalLogLevel := getKlogLevel()
require.GreaterOrEqual(t, int(originalLogLevel), int(klog.Level(0)), "cannot get klog level")
tests := []struct {
name string
level LogLevel
wantLevel klog.Level
wantEnabled []LogLevel
wantErr string
}{
{
name: "unset",
wantLevel: 0,
wantEnabled: []LogLevel{LevelWarning},
},
{
name: "warning",
level: LevelWarning,
wantLevel: 0,
wantEnabled: []LogLevel{LevelWarning},
},
{
name: "info",
level: LevelInfo,
wantLevel: 2,
wantEnabled: []LogLevel{LevelWarning, LevelInfo},
},
{
name: "debug",
level: LevelDebug,
wantLevel: 4,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug},
},
{
name: "trace",
level: LevelTrace,
wantLevel: 6,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace},
},
{
name: "all",
level: LevelAll,
wantLevel: 108,
wantEnabled: []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll},
},
{
name: "invalid level",
level: "panda",
wantLevel: originalLogLevel,
wantErr: errInvalidLogLevel.Error(),
},
}
for _, tt := range tests {
tt := tt // capture range variable
t.Run(tt.name, func(t *testing.T) {
defer func() {
undoGlobalLogLevelChanges(t, originalLogLevel)
}()
err := ValidateAndSetLogLevelGlobally(tt.level)
require.Equal(t, tt.wantErr, errString(err))
require.Equal(t, tt.wantLevel, getKlogLevel())
if tt.wantEnabled != nil {
allLevels := []LogLevel{LevelWarning, LevelInfo, LevelDebug, LevelTrace, LevelAll}
for _, level := range allLevels {
if contains(tt.wantEnabled, level) {
require.Truef(t, Enabled(level), "wanted %q to be enabled", level)
} else {
require.False(t, Enabled(level), "did not want %q to be enabled", level)
}
}
}
})
}
require.Equal(t, originalLogLevel, getKlogLevel())
}
func contains(haystack []LogLevel, needle LogLevel) bool {
for _, hay := range haystack {
if hay == needle {
return true
}
}
return false
}
func errString(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func undoGlobalLogLevelChanges(t *testing.T, originalLogLevel klog.Level) {
t.Helper()
_, err := logs.GlogSetter(strconv.Itoa(int(originalLogLevel)))
require.NoError(t, err)
}
func getKlogLevel() klog.Level {
// hack around klog not exposing a Get method
for i := klog.Level(0); i < 256; i++ {
if klog.V(i).Enabled() {
continue
}
return i - 1
}
return -1
}

View File

@ -1,7 +1,7 @@
// Copyright 2020-2021 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 logr to help enforce pinniped's logging convention.
// Logs are always structured as a constant message with key and value pairs of related metadata. // Logs are always structured as a constant message with key and value pairs of related metadata.
// //
// The logging levels in order of increasing verbosity are: // The logging levels in order of increasing verbosity are:
@ -26,10 +26,18 @@
// 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 (
"os"
const errorKey = "error" "github.com/go-logr/logr"
)
const errorKey = "error" // this matches zapr's default for .Error calls (which is asserted via tests)
// Logger implements the plog logging convention described above. The global functions in this package
// such as Info should be used when one does not intend to write tests assertions for specific log messages.
// If test assertions are desired, Logger should be passed in as an input. New should be used as the
// production implementation and TestLogger should be used to write test assertions.
type Logger interface { type Logger interface {
Error(msg string, err error, keysAndValues ...interface{}) Error(msg string, err error, keysAndValues ...interface{})
Warning(msg string, keysAndValues ...interface{}) Warning(msg string, keysAndValues ...interface{})
@ -41,100 +49,155 @@ type Logger interface {
Trace(msg string, keysAndValues ...interface{}) Trace(msg string, keysAndValues ...interface{})
TraceErr(msg string, err error, keysAndValues ...interface{}) TraceErr(msg string, err error, keysAndValues ...interface{})
All(msg string, keysAndValues ...interface{}) All(msg string, keysAndValues ...interface{})
Always(msg string, keysAndValues ...interface{})
WithValues(keysAndValues ...interface{}) Logger
WithName(name string) Logger
// does not include Fatal on purpose because that is not a method you should be using
// for internal and test use only
withDepth(d int) Logger
withLogrMod(mod func(logr.Logger) logr.Logger) Logger
} }
// MinLogger is the overlap between Logger and logr.Logger.
type MinLogger interface {
Info(msg string, keysAndValues ...interface{})
}
var _ Logger = pLogger{}
var _, _, _ MinLogger = pLogger{}, logr.Logger{}, Logger(nil)
type pLogger struct { type pLogger struct {
prefix string mods []func(logr.Logger) logr.Logger
depth int depth int
} }
func New(prefix string) Logger { func New() Logger {
return &pLogger{ return pLogger{}
depth: 0,
prefix: prefix,
}
} }
func (p *pLogger) Error(msg string, err error, keysAndValues ...interface{}) { func (p pLogger) Error(msg string, err error, keysAndValues ...interface{}) {
klog.ErrorSDepth(p.depth+1, err, p.prefix+msg, keysAndValues...) p.logr().WithCallDepth(p.depth+1).Error(err, msg, keysAndValues...)
} }
func (p *pLogger) warningDepth(msg string, depth int, keysAndValues ...interface{}) { func (p pLogger) warningDepth(msg string, depth int, keysAndValues ...interface{}) {
if p.logr().V(klogLevelWarning).Enabled() {
// 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...)
if klog.V(klogLevelWarning).Enabled() { p.logr().V(klogLevelWarning).WithCallDepth(depth+1).Info(msg, keysAndValues...)
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...)
} }
} }
func (p *pLogger) Warning(msg string, keysAndValues ...interface{}) { func (p pLogger) Warning(msg string, keysAndValues ...interface{}) {
p.warningDepth(msg, p.depth+1, keysAndValues...) 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{}) {
func (p *pLogger) WarningErr(msg string, err error, keysAndValues ...interface{}) {
p.warningDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) p.warningDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
} }
func (p *pLogger) infoDepth(msg string, depth int, keysAndValues ...interface{}) { func (p pLogger) infoDepth(msg string, depth int, keysAndValues ...interface{}) {
if klog.V(klogLevelInfo).Enabled() { if p.logr().V(KlogLevelInfo).Enabled() {
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...) p.logr().V(KlogLevelInfo).WithCallDepth(depth+1).Info(msg, keysAndValues...)
} }
} }
func (p *pLogger) Info(msg string, keysAndValues ...interface{}) { func (p pLogger) Info(msg string, keysAndValues ...interface{}) {
p.infoDepth(msg, p.depth+1, keysAndValues...) 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{}) {
func (p *pLogger) InfoErr(msg string, err error, keysAndValues ...interface{}) {
p.infoDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) p.infoDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
} }
func (p *pLogger) debugDepth(msg string, depth int, keysAndValues ...interface{}) { func (p pLogger) debugDepth(msg string, depth int, keysAndValues ...interface{}) {
if klog.V(klogLevelDebug).Enabled() { if p.logr().V(KlogLevelDebug).Enabled() {
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...) p.logr().V(KlogLevelDebug).WithCallDepth(depth+1).Info(msg, keysAndValues...)
} }
} }
func (p *pLogger) Debug(msg string, keysAndValues ...interface{}) { func (p pLogger) Debug(msg string, keysAndValues ...interface{}) {
p.debugDepth(msg, p.depth+1, keysAndValues...) 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{}) {
func (p *pLogger) DebugErr(msg string, err error, keysAndValues ...interface{}) {
p.debugDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) p.debugDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
} }
func (p *pLogger) traceDepth(msg string, depth int, keysAndValues ...interface{}) { func (p pLogger) traceDepth(msg string, depth int, keysAndValues ...interface{}) {
if klog.V(klogLevelTrace).Enabled() { if p.logr().V(KlogLevelTrace).Enabled() {
klog.InfoSDepth(depth+1, p.prefix+msg, keysAndValues...) p.logr().V(KlogLevelTrace).WithCallDepth(depth+1).Info(msg, keysAndValues...)
} }
} }
func (p *pLogger) Trace(msg string, keysAndValues ...interface{}) { func (p pLogger) Trace(msg string, keysAndValues ...interface{}) {
p.traceDepth(msg, p.depth+1, keysAndValues...) 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{}) {
func (p *pLogger) TraceErr(msg string, err error, keysAndValues ...interface{}) {
p.traceDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...) p.traceDepth(msg, p.depth+1, append([]interface{}{errorKey, err}, keysAndValues...)...)
} }
func (p *pLogger) All(msg string, keysAndValues ...interface{}) { func (p pLogger) All(msg string, keysAndValues ...interface{}) {
if klog.V(klogLevelAll).Enabled() { if p.logr().V(klogLevelAll).Enabled() {
klog.InfoSDepth(p.depth+1, p.prefix+msg, keysAndValues...) p.logr().V(klogLevelAll).WithCallDepth(p.depth+1).Info(msg, keysAndValues...)
} }
} }
var logger Logger = &pLogger{ //nolint:gochecknoglobals func (p pLogger) Always(msg string, keysAndValues ...interface{}) {
depth: 1, p.logr().WithCallDepth(p.depth+1).Info(msg, keysAndValues...)
} }
// Use Error to log an unexpected system error. func (p pLogger) WithValues(keysAndValues ...interface{}) Logger {
if len(keysAndValues) == 0 {
return p
}
return p.withLogrMod(func(l logr.Logger) logr.Logger {
return l.WithValues(keysAndValues...)
})
}
func (p pLogger) WithName(name string) Logger {
if len(name) == 0 {
return p
}
return p.withLogrMod(func(l logr.Logger) logr.Logger {
return l.WithName(name)
})
}
func (p pLogger) withDepth(d int) Logger {
out := p
out.depth += d // out is a copy so this does not mutate p
return out
}
func (p pLogger) withLogrMod(mod func(logr.Logger) logr.Logger) Logger {
out := p // make a copy and carefully avoid mutating the mods slice
mods := make([]func(logr.Logger) logr.Logger, 0, len(out.mods)+1)
mods = append(mods, out.mods...)
mods = append(mods, mod)
out.mods = mods
return out
}
func (p pLogger) logr() logr.Logger {
l := Logr() // grab the current global logger and its current config
for _, mod := range p.mods {
mod := mod
l = mod(l) // and then update it with all modifications
}
return l // this logger is guaranteed to have the latest config and all modifications
}
var logger = New().withDepth(1) //nolint:gochecknoglobals
func Error(msg string, err error, keysAndValues ...interface{}) { func Error(msg string, err error, keysAndValues ...interface{}) {
logger.Error(msg, err, keysAndValues...) logger.Error(msg, err, keysAndValues...)
} }
@ -143,7 +206,6 @@ func Warning(msg string, keysAndValues ...interface{}) {
logger.Warning(msg, keysAndValues...) logger.Warning(msg, keysAndValues...)
} }
// 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{}) {
logger.WarningErr(msg, err, keysAndValues...) logger.WarningErr(msg, err, keysAndValues...)
} }
@ -152,7 +214,6 @@ func Info(msg string, keysAndValues ...interface{}) {
logger.Info(msg, keysAndValues...) logger.Info(msg, keysAndValues...)
} }
// 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{}) {
logger.InfoErr(msg, err, keysAndValues...) logger.InfoErr(msg, err, keysAndValues...)
} }
@ -161,7 +222,6 @@ func Debug(msg string, keysAndValues ...interface{}) {
logger.Debug(msg, keysAndValues...) logger.Debug(msg, keysAndValues...)
} }
// 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{}) {
logger.DebugErr(msg, err, keysAndValues...) logger.DebugErr(msg, err, keysAndValues...)
} }
@ -170,7 +230,6 @@ func Trace(msg string, keysAndValues ...interface{}) {
logger.Trace(msg, keysAndValues...) logger.Trace(msg, keysAndValues...)
} }
// 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{}) {
logger.TraceErr(msg, err, keysAndValues...) logger.TraceErr(msg, err, keysAndValues...)
} }
@ -178,3 +237,23 @@ func TraceErr(msg string, err error, keysAndValues ...interface{}) {
func All(msg string, keysAndValues ...interface{}) { func All(msg string, keysAndValues ...interface{}) {
logger.All(msg, keysAndValues...) logger.All(msg, keysAndValues...)
} }
func Always(msg string, keysAndValues ...interface{}) {
logger.Always(msg, keysAndValues...)
}
func WithValues(keysAndValues ...interface{}) Logger {
// this looks weird but it is the same as New().WithValues(keysAndValues...) because it returns a new logger rooted at the call site
return logger.withDepth(-1).WithValues(keysAndValues...)
}
func WithName(name string) Logger {
// this looks weird but it is the same as New().WithName(name) because it returns a new logger rooted at the call site
return logger.withDepth(-1).WithName(name)
}
func Fatal(err error, keysAndValues ...interface{}) {
logger.Error("unrecoverable error encountered", err, keysAndValues...)
globalFlush()
os.Exit(1)
}

366
internal/plog/plog_test.go Normal file
View File

@ -0,0 +1,366 @@
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestPlog(t *testing.T) {
tests := []struct {
name string
run func(Logger)
want string
}{
{
name: "basic",
run: testAllPlogMethods,
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"always","panda":2}
`,
},
{
name: "with values",
run: func(l Logger) {
testAllPlogMethods(l.WithValues("hi", 42))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"e","hi":42,"panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"w","hi":42,"warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"we","hi":42,"warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"i","hi":42,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"ie","hi":42,"error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"d","hi":42,"panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"de","hi":42,"error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"t","hi":42,"panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"te","hi":42,"error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"all","hi":42,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"always","hi":42,"panda":2}
`,
},
{
name: "with values conflict", // duplicate key is included twice ...
run: func(l Logger) {
testAllPlogMethods(l.WithValues("panda", false))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"e","panda":false,"panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"w","panda":false,"warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"we","panda":false,"warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"i","panda":false,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"ie","panda":false,"error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"d","panda":false,"panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"de","panda":false,"error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"t","panda":false,"panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"te","panda":false,"error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"all","panda":false,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"always","panda":false,"panda":2}
`,
},
{
name: "with values nested",
run: func(l Logger) {
testAllPlogMethods(l.WithValues("hi", 42).WithValues("not", time.Hour))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"e","hi":42,"not":"1h0m0s","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"w","hi":42,"not":"1h0m0s","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"we","hi":42,"not":"1h0m0s","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"i","hi":42,"not":"1h0m0s","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"ie","hi":42,"not":"1h0m0s","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"d","hi":42,"not":"1h0m0s","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"de","hi":42,"not":"1h0m0s","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"t","hi":42,"not":"1h0m0s","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"te","hi":42,"not":"1h0m0s","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"all","hi":42,"not":"1h0m0s","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"always","hi":42,"not":"1h0m0s","panda":2}
`,
},
{
name: "with name",
run: func(l Logger) {
testAllPlogMethods(l.WithName("yoyo"))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"always","panda":2}
`,
},
{
name: "with name nested",
run: func(l Logger) {
testAllPlogMethods(l.WithName("yoyo").WithName("gold"))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"yoyo.gold","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"always","panda":2}
`,
},
{
name: "depth 3",
run: func(l Logger) {
testAllPlogMethods(l.withDepth(3))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"testing/testing.go:<line>$testing.tRunner","message":"always","panda":2}
`,
},
{
name: "depth 2",
run: func(l Logger) {
testAllPlogMethods(l.withDepth(2))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func15","message":"always","panda":2}
`,
},
{
name: "depth 1",
run: func(l Logger) {
testAllPlogMethods(l.withDepth(1))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func8","message":"always","panda":2}
`,
},
{
name: "depth 0",
run: func(l Logger) {
testAllPlogMethods(l.withDepth(0))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.testAllPlogMethods","message":"always","panda":2}
`,
},
{
name: "depth -1",
run: func(l Logger) {
testAllPlogMethods(l.withDepth(-1))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Error","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Warning","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.WarningErr","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Info","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.InfoErr","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Debug","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.DebugErr","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Trace","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.TraceErr","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.All","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Always","message":"always","panda":2}
`,
},
{
name: "depth -2",
run: func(l Logger) {
testAllPlogMethods(l.withDepth(-2))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Error","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.warningDepth","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.warningDepth","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.infoDepth","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.infoDepth","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.debugDepth","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.debugDepth","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.traceDepth","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.traceDepth","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"always","panda":2}
`,
},
{
name: "depth -3",
run: func(l Logger) {
testAllPlogMethods(l.withDepth(-3))
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.2.3/zapr.go:<line>$zapr.(*zapLogger).Error","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.2.3/logr.go:<line>$logr.Logger.Info","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.2.3/zapr.go:<line>$zapr.(*zapLogger).Info","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.2.3/zapr.go:<line>$zapr.(*zapLogger).Info","message":"always","panda":2}
`,
},
{
name: "closure",
run: func(l Logger) {
func() {
func() {
testErr := fmt.Errorf("some err")
l.Error("e", testErr, "panda", 2)
l.Warning("w", "panda", 2)
l.WarningErr("we", testErr, "panda", 2)
l.Info("i", "panda", 2)
l.InfoErr("ie", testErr, "panda", 2)
l.Debug("d", "panda", 2)
l.DebugErr("de", testErr, "panda", 2)
l.Trace("t", "panda", 2)
l.TraceErr("te", testErr, "panda", 2)
l.All("all", "panda", 2)
l.Always("always", "panda", 2)
}()
}()
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog_test.go:<line>$plog.TestPlog.func13.1.1","message":"always","panda":2}
`,
},
{
name: "closure depth -1",
run: func(l Logger) {
func() {
func() {
testErr := fmt.Errorf("some err")
l = l.withDepth(-1)
l.Error("e", testErr, "panda", 2)
l.Warning("w", "panda", 2)
l.WarningErr("we", testErr, "panda", 2)
l.Info("i", "panda", 2)
l.InfoErr("ie", testErr, "panda", 2)
l.Debug("d", "panda", 2)
l.DebugErr("de", testErr, "panda", 2)
l.Trace("t", "panda", 2)
l.TraceErr("te", testErr, "panda", 2)
l.All("all", "panda", 2)
l.Always("always", "panda", 2)
}()
}()
},
want: `
{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Error","message":"e","panda":2,"error":"some err"}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Warning","message":"w","warning":true,"panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.WarningErr","message":"we","warning":true,"error":"some err","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Info","message":"i","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.InfoErr","message":"ie","error":"some err","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Debug","message":"d","panda":2}
{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.DebugErr","message":"de","error":"some err","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Trace","message":"t","panda":2}
{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.TraceErr","message":"te","error":"some err","panda":2}
{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.All","message":"all","panda":2}
{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:<line>$plog.pLogger.Always","message":"always","panda":2}
`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var log bytes.Buffer
tt.run(TestLogger(t, &log))
require.Equal(t, strings.TrimSpace(tt.want), strings.TrimSpace(log.String()))
})
}
}
func testAllPlogMethods(l Logger) {
testErr := fmt.Errorf("some err")
l.Error("e", testErr, "panda", 2)
l.Warning("w", "panda", 2)
l.WarningErr("we", testErr, "panda", 2)
l.Info("i", "panda", 2)
l.InfoErr("ie", testErr, "panda", 2)
l.Debug("d", "panda", 2)
l.DebugErr("de", testErr, "panda", 2)
l.Trace("t", "panda", 2)
l.TraceErr("te", testErr, "panda", 2)
l.All("all", "panda", 2)
l.Always("always", "panda", 2)
}

120
internal/plog/testing.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"context"
"io"
"math"
"strings"
"testing"
"time"
"github.com/go-logr/logr"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"k8s.io/utils/clock"
clocktesting "k8s.io/utils/clock/testing"
)
// contextKey type is unexported to prevent collisions.
type contextKey int
const zapOverridesKey contextKey = iota
func TestZapOverrides(ctx context.Context, t *testing.T, w io.Writer, f func(*zap.Config), opts ...zap.Option) context.Context {
t.Helper() // discourage use outside of tests
overrides := &testOverrides{
t: t,
w: w,
f: f,
opts: opts,
}
return context.WithValue(ctx, zapOverridesKey, overrides)
}
func TestLogger(t *testing.T, w io.Writer) Logger {
t.Helper()
return New().withLogrMod(func(l logr.Logger) logr.Logger {
return l.WithSink(TestZapr(t, w).GetSink())
})
}
func TestZapr(t *testing.T, w io.Writer) logr.Logger {
t.Helper()
now, err := time.Parse(time.RFC3339Nano, "2099-08-08T13:57:36.123456789Z")
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
ctx = TestZapOverrides(ctx, t, w,
func(config *zap.Config) {
config.Level = zap.NewAtomicLevelAt(math.MinInt8) // log everything during tests
// make test assertions less painful to write while keeping them as close to the real thing as possible
config.EncoderConfig.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
trimmed := caller.TrimmedPath()
if idx := strings.LastIndexByte(trimmed, ':'); idx != -1 {
trimmed = trimmed[:idx+1] + "<line>"
}
enc.AppendString(trimmed + funcEncoder(caller))
}
},
zap.WithClock(ZapClock(clocktesting.NewFakeClock(now))), // have the clock be static during tests
zap.AddStacktrace(nopLevelEnabler{}), // do not log stacktraces
)
// there is no buffering so we can ignore flush
zl, _, err := newLogr(ctx, "json", 0)
require.NoError(t, err)
return zl
}
var _ zapcore.Clock = &clockAdapter{}
type clockAdapter struct {
clock clock.Clock
}
func (c *clockAdapter) Now() time.Time {
return c.clock.Now()
}
func (c *clockAdapter) NewTicker(duration time.Duration) *time.Ticker {
return &time.Ticker{C: c.clock.Tick(duration)}
}
func ZapClock(c clock.Clock) zapcore.Clock {
return &clockAdapter{clock: c}
}
var _ zap.Sink = nopCloserSink{}
type nopCloserSink struct{ zapcore.WriteSyncer }
func (nopCloserSink) Close() error { return nil }
// newSink returns a wrapper around the input writer that is safe for concurrent use.
func newSink(w io.Writer) zap.Sink {
return nopCloserSink{WriteSyncer: zapcore.Lock(zapcore.AddSync(w))}
}
var _ zapcore.LevelEnabler = nopLevelEnabler{}
type nopLevelEnabler struct{}
func (nopLevelEnabler) Enabled(_ zapcore.Level) bool { return false }
type testOverrides struct {
t *testing.T
w io.Writer
f func(*zap.Config)
opts []zap.Option
}

187
internal/plog/zap.go Normal file
View File

@ -0,0 +1,187 @@
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package plog
import (
"context"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/go-logr/logr"
"github.com/go-logr/zapr"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/klog/v2"
"k8s.io/klog/v2/textlogger"
)
func newLogr(ctx context.Context, encoding string, klogLevel klog.Level) (logr.Logger, func(), error) {
if encoding == "text" {
var w io.Writer = os.Stderr
flush := func() { _ = os.Stderr.Sync() }
// allow tests to override klog config (but cheat and re-use the zap override key)
if overrides, ok := ctx.Value(zapOverridesKey).(*testOverrides); ok {
if overrides.w != nil {
w = newSink(overrides.w) // make sure the value is safe for concurrent use
flush = func() {}
}
}
return textlogger.NewLogger(textlogger.NewConfig(textlogger.Verbosity(int(klogLevel)), textlogger.Output(w))), flush, nil
}
path := "stderr" // this is how zap refers to os.Stderr
f := func(config *zap.Config) {
if encoding == "console" {
config.EncoderConfig.LevelKey = zapcore.OmitKey
config.EncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
config.EncoderConfig.EncodeTime = humanTimeEncoder
config.EncoderConfig.EncodeDuration = humanDurationEncoder
}
}
var opts []zap.Option
// allow tests to override zap config
if overrides, ok := ctx.Value(zapOverridesKey).(*testOverrides); ok {
if overrides.w != nil {
// use a per invocation random string as the key into the global map
testKey := "/" + base64.RawURLEncoding.EncodeToString([]byte(rand.String(32)))
// tell zap to use our custom sink registry to find the writer
path = "pinniped://" + testKey
// the registry may be called multiple times so make sure the value is safe for concurrent use
sink := newSink(overrides.w)
// store the test's buffer where we can find it globally
actual, loaded := sinkMap.LoadOrStore(testKey, sink)
require.False(overrides.t, loaded)
require.Equal(overrides.t, sink, actual)
defer func() {
// delete buffer from the global map to prevent a memory leak
value, loaded := sinkMap.LoadAndDelete(testKey)
require.True(overrides.t, loaded)
require.Equal(overrides.t, sink, value)
}()
}
if overrides.f != nil {
f = overrides.f
}
if overrides.opts != nil {
opts = overrides.opts
}
}
// when using the trace or all log levels, an error log will contain the full stack.
// this is too noisy for regular use because things like leader election conflicts
// result in transient errors and we do not want all of that noise in the logs.
// this check is performed dynamically on the global log level.
return newZapr(globalLevel, LevelTrace, encoding, path, f, opts...)
}
func newZapr(level zap.AtomicLevel, addStack zapcore.LevelEnabler, encoding, path string, f func(config *zap.Config), opts ...zap.Option) (logr.Logger, func(), error) {
opts = append([]zap.Option{zap.AddStacktrace(addStack)}, opts...)
config := zap.Config{
Level: level,
Development: false,
DisableCaller: false,
DisableStacktrace: true, // handled via the AddStacktrace call above
Sampling: nil, // keep all logs for now
Encoding: encoding,
EncoderConfig: zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "level",
TimeKey: "timestamp",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey, // included in caller
StacktraceKey: "stacktrace",
SkipLineEnding: false,
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: levelEncoder,
// human-readable and machine parsable with microsecond precision (same as klog, kube audit event, etc)
EncodeTime: zapcore.TimeEncoderOfLayout(metav1.RFC3339Micro),
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: callerEncoder,
EncodeName: nil,
NewReflectedEncoder: nil,
ConsoleSeparator: " ",
},
OutputPaths: []string{path},
ErrorOutputPaths: []string{path},
InitialFields: nil,
}
f(&config)
log, err := config.Build(opts...)
if err != nil {
return logr.Logger{}, nil, fmt.Errorf("failed to build zap logger: %w", err)
}
return zapr.NewLogger(log), func() { _ = log.Sync() }, nil
}
func levelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
plogLevel := zapLevelToPlogLevel(l)
if len(plogLevel) == 0 {
return // this tells zap that it should handle encoding the level itself because we do not know the mapping
}
enc.AppendString(string(plogLevel))
}
func zapLevelToPlogLevel(l zapcore.Level) LogLevel {
if l > 0 {
// best effort mapping, the zap levels do not really translate to klog
// but this is correct for "error" level which is all we need for logr
return LogLevel(l.String())
}
// klog levels are inverted when zap handles them
switch {
case -l >= klogLevelAll:
return LevelAll
case -l >= KlogLevelTrace:
return LevelTrace
case -l >= KlogLevelDebug:
return LevelDebug
case -l >= KlogLevelInfo:
return LevelInfo
default:
return "" // warning is handled via a custom key since klog level 0 is ambiguous
}
}
func callerEncoder(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(caller.String() + funcEncoder(caller))
}
func funcEncoder(caller zapcore.EntryCaller) string {
funcName := caller.Function
if idx := strings.LastIndexByte(funcName, '/'); idx != -1 {
funcName = funcName[idx+1:] // keep everything after the last /
}
return "$" + funcName
}
func humanDurationEncoder(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(duration.HumanDuration(d))
}
func humanTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Local().Format(time.RFC1123))
}

View File

@ -71,7 +71,7 @@ func TestCreate(t *testing.T) {
it.Before(func() { it.Before(func() {
r = require.New(t) r = require.New(t)
ctrl = gomock.NewController(t) ctrl = gomock.NewController(t)
logger = testutil.NewTranscriptLogger(t) logger = testutil.NewTranscriptLogger(t) // nolint: staticcheck // old test with lots of log statements
klog.SetLogger(logr.New(logger)) // this is unfortunately a global logger, so can't run these tests in parallel :( klog.SetLogger(logr.New(logger)) // this is unfortunately a global logger, so can't run these tests in parallel :(
}) })

View File

@ -22,14 +22,12 @@ import (
"github.com/joshlf/go-acl" "github.com/joshlf/go-acl"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apimachineryversion "k8s.io/apimachinery/pkg/version"
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/pkg/version" "k8s.io/client-go/pkg/version"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
"k8s.io/klog/v2/klogr"
"k8s.io/utils/clock" "k8s.io/utils/clock"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
@ -272,7 +270,7 @@ func prepareControllers(
pinnipedClient, pinnipedClient,
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
secretInformer, secretInformer,
klogr.New(), plog.Logr(), // nolint: staticcheck // old controller with lots of log statements
controllerlib.WithInformer, controllerlib.WithInformer,
), ),
singletonWorker). singletonWorker).
@ -315,7 +313,7 @@ func startControllers(ctx context.Context, shutdown *sync.WaitGroup, buildContro
} }
//nolint:funlen //nolint:funlen
func runSupervisor(podInfo *downward.PodInfo, cfg *supervisor.Config) error { func runSupervisor(ctx context.Context, podInfo *downward.PodInfo, cfg *supervisor.Config) error {
serverInstallationNamespace := podInfo.Namespace serverInstallationNamespace := podInfo.Namespace
dref, supervisorDeployment, supervisorPod, err := deploymentref.New(podInfo) dref, supervisorDeployment, supervisorPod, err := deploymentref.New(podInfo)
@ -389,7 +387,6 @@ func runSupervisor(podInfo *downward.PodInfo, cfg *supervisor.Config) error {
leaderElector, leaderElector,
) )
ctx := signalCtx()
shutdown := &sync.WaitGroup{} shutdown := &sync.WaitGroup{}
if err := startControllers(ctx, shutdown, buildControllersFunc); err != nil { if err := startControllers(ctx, shutdown, buildControllersFunc); err != nil {
@ -504,12 +501,14 @@ func maybeSetupUnixPerms(endpoint *supervisor.Endpoint, pod *corev1.Pod) func()
} }
} }
func main() error { // return an error instead of klog.Fatal to allow defer statements to run func main() error { // return an error instead of plog.Fatal to allow defer statements to run
logs.InitLogs() defer plog.Setup()()
defer logs.FlushLogs()
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get()) plog.Always("Running supervisor",
klog.Infof("Command-line arguments were: %s %s %s", os.Args[0], os.Args[1], os.Args[2]) "user-agent", rest.DefaultKubernetesUserAgent(),
"version", versionInfo(version.Get()),
"arguments", os.Args,
)
// Discover in which namespace we are installed. // Discover in which namespace we are installed.
podInfo, err := downward.Load(os.Args[1]) podInfo, err := downward.Load(os.Args[1])
@ -517,17 +516,21 @@ func main() error { // return an error instead of klog.Fatal to allow defer stat
return fmt.Errorf("could not read pod metadata: %w", err) return fmt.Errorf("could not read pod metadata: %w", err)
} }
ctx := signalCtx()
// Read the server config file. // Read the server config file.
cfg, err := supervisor.FromPath(os.Args[2]) cfg, err := supervisor.FromPath(ctx, os.Args[2])
if err != nil { if err != nil {
return fmt.Errorf("could not load config: %w", err) return fmt.Errorf("could not load config: %w", err)
} }
return runSupervisor(podInfo, cfg) return runSupervisor(ctx, podInfo, cfg)
} }
func Main() { func Main() {
if err := main(); err != nil { if err := main(); err != nil {
klog.Fatal(err) plog.Fatal(err)
} }
} }
type versionInfo apimachineryversion.Info // hide .String() method from plog

View File

@ -1,4 +1,4 @@
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package testutil package testutil
@ -54,9 +54,47 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret
require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets) require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets)
} }
func RequireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) { func RequireSecurityHeadersWithFormPostPageCSPs(t *testing.T, response *httptest.ResponseRecorder) {
// This is a more relaxed assertion rather than an exact match, so it can cover all the CSP headers we use. // Loosely confirm that the unique CSPs needed for the form_post page were used.
require.Contains(t, response.Header().Get("Content-Security-Policy"), "default-src 'none'") cspHeader := response.Header().Get("Content-Security-Policy")
require.Contains(t, cspHeader, "script-src '") // loose assertion
require.Contains(t, cspHeader, "style-src '") // loose assertion
require.Contains(t, cspHeader, "img-src data:")
require.Contains(t, cspHeader, "connect-src *")
// Also require all the usual security headers.
requireSecurityHeaders(t, response)
}
func RequireSecurityHeadersWithLoginPageCSPs(t *testing.T, response *httptest.ResponseRecorder) {
// Loosely confirm that the unique CSPs needed for the login page were used.
cspHeader := response.Header().Get("Content-Security-Policy")
require.Contains(t, cspHeader, "style-src '") // loose assertion
require.NotContains(t, cspHeader, "script-src") // only needed by form_post page
require.NotContains(t, cspHeader, "img-src data:") // only needed by form_post page
require.NotContains(t, cspHeader, "connect-src *") // only needed by form_post page
// Also require all the usual security headers.
requireSecurityHeaders(t, response)
}
func RequireSecurityHeadersWithoutCustomCSPs(t *testing.T, response *httptest.ResponseRecorder) {
// Confirm that the unique CSPs needed for the form_post or login page were NOT used.
cspHeader := response.Header().Get("Content-Security-Policy")
require.NotContains(t, cspHeader, "script-src")
require.NotContains(t, cspHeader, "style-src")
require.NotContains(t, cspHeader, "img-src data:")
require.NotContains(t, cspHeader, "connect-src *")
// Also require all the usual security headers.
requireSecurityHeaders(t, response)
}
func requireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) {
// Loosely confirm that the generic default CSPs were used.
cspHeader := response.Header().Get("Content-Security-Policy")
require.Contains(t, cspHeader, "default-src 'none'")
require.Contains(t, cspHeader, "frame-ancestors 'none'")
require.Equal(t, "DENY", response.Header().Get("X-Frame-Options")) require.Equal(t, "DENY", response.Header().Get("X-Frame-Options"))
require.Equal(t, "1; mode=block", response.Header().Get("X-XSS-Protection")) require.Equal(t, "1; mode=block", response.Header().Get("X-XSS-Protection"))

File diff suppressed because one or more lines are too long

View File

@ -15,6 +15,7 @@ import (
"time" "time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc" coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/securecookie"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -830,12 +831,51 @@ func NewTestUpstreamOIDCIdentityProviderBuilder() *TestUpstreamOIDCIdentityProvi
type ExpectedUpstreamStateParamFormat struct { type ExpectedUpstreamStateParamFormat struct {
P string `json:"p"` P string `json:"p"`
U string `json:"u"` U string `json:"u"`
T string `json:"t"`
N string `json:"n"` N string `json:"n"`
C string `json:"c"` C string `json:"c"`
K string `json:"k"` K string `json:"k"`
V string `json:"v"` V string `json:"v"`
} }
type UpstreamStateParamBuilder ExpectedUpstreamStateParamFormat
func (b UpstreamStateParamBuilder) Build(t *testing.T, stateEncoder *securecookie.SecureCookie) string {
state, err := stateEncoder.Encode("s", b)
require.NoError(t, err)
return state
}
func (b *UpstreamStateParamBuilder) WithAuthorizeRequestParams(params string) *UpstreamStateParamBuilder {
b.P = params
return b
}
func (b *UpstreamStateParamBuilder) WithNonce(nonce string) *UpstreamStateParamBuilder {
b.N = nonce
return b
}
func (b *UpstreamStateParamBuilder) WithCSRF(csrf string) *UpstreamStateParamBuilder {
b.C = csrf
return b
}
func (b *UpstreamStateParamBuilder) WithPKCE(pkce string) *UpstreamStateParamBuilder {
b.K = pkce
return b
}
func (b *UpstreamStateParamBuilder) WithUpstreamIDPType(upstreamIDPType string) *UpstreamStateParamBuilder {
b.T = upstreamIDPType
return b
}
func (b *UpstreamStateParamBuilder) WithStateVersion(version string) *UpstreamStateParamBuilder {
b.V = version
return b
}
type staticKeySet struct { type staticKeySet struct {
publicKey crypto.PublicKey publicKey crypto.PublicKey
} }

View File

@ -24,14 +24,14 @@ type Logger struct {
buffer syncBuffer buffer syncBuffer
} }
// New returns a new test Logger. Use this for all new tests. // Deprecated: Use plog.TestLogger or plog.TestZapr instead. This is meant for old tests only.
func New(t *testing.T) *Logger { func New(t *testing.T) *Logger {
res := Logger{t: t} res := Logger{t: t}
res.Logger = stdr.New(log.New(&res.buffer, "", 0)) res.Logger = stdr.New(log.New(&res.buffer, "", 0))
return &res return &res
} }
// Deprecated: NewLegacy returns a new test Logger. Use this for old tests if necessary. // Deprecated: Use plog.TestLogger or plog.TestZapr instead. This is meant for old tests only.
func NewLegacy(t *testing.T) *Logger { func NewLegacy(t *testing.T) *Logger {
res := New(t) res := New(t)
res.Logger = newStdLogger(log.New(&res.buffer, "", 0)) res.Logger = newStdLogger(log.New(&res.buffer, "", 0))

View File

@ -24,6 +24,7 @@ type TranscriptLogMessage struct {
Message string Message string
} }
// Deprecated: Use plog.TestLogger or plog.TestZapr instead. This is meant for old tests only.
func NewTranscriptLogger(t *testing.T) *TranscriptLogger { func NewTranscriptLogger(t *testing.T) *TranscriptLogger {
return &TranscriptLogger{t: t} return &TranscriptLogger{t: t}
} }

View File

@ -411,13 +411,13 @@ func (p *ProviderConfig) maybeFetchUserInfo(ctx context.Context, tok *oauth2.Tok
func maybeLogClaims(msg, name string, claims map[string]interface{}) { func maybeLogClaims(msg, name string, claims map[string]interface{}) {
if plog.Enabled(plog.LevelAll) { // log keys and values at all level if plog.Enabled(plog.LevelAll) { // log keys and values at all level
data, _ := json.Marshal(claims) // nothing we can do if it fails, but it really never should data, _ := json.Marshal(claims) // nothing we can do if it fails, but it really never should
plog.Info(msg, "providerName", name, "claims", string(data)) plog.All(msg, "providerName", name, "claims", string(data))
return return
} }
if plog.Enabled(plog.LevelDebug) { // log keys at debug level if plog.Enabled(plog.LevelDebug) { // log keys at debug level
keys := sets.StringKeySet(claims).List() // note: this is only safe because the compiler asserts that claims is a map[string]<anything> keys := sets.StringKeySet(claims).List() // note: this is only safe because the compiler asserts that claims is a map[string]<anything>
plog.Info(msg, "providerName", name, "keys", keys) plog.Debug(msg, "providerName", name, "keys", keys)
return return
} }
} }

View File

@ -33,6 +33,7 @@ import (
"go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/httputil/securityheader"
"go.pinniped.dev/internal/net/phttp" "go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/internal/upstreamoidc"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
@ -63,8 +64,6 @@ const (
defaultPasswordEnvVarName = "PINNIPED_PASSWORD" //nolint:gosec // this is not a credential defaultPasswordEnvVarName = "PINNIPED_PASSWORD" //nolint:gosec // this is not a credential
httpLocationHeaderName = "Location" httpLocationHeaderName = "Location"
debugLogLevel = 4
) )
// stdin returns the file descriptor for stdin as an int. // stdin returns the file descriptor for stdin as an int.
@ -356,7 +355,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.") h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Found unexpired cached token.")
return cached, nil return cached, nil
} }
@ -520,7 +519,7 @@ func (h *handlerState) getUsernameAndPassword() (string, string, error) {
return "", "", fmt.Errorf("error prompting for username: %w", err) return "", "", fmt.Errorf("error prompting for username: %w", err)
} }
} else { } else {
h.logger.V(debugLogLevel).Info("Pinniped: Read username from environment variable", "name", defaultUsernameEnvVarName) h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Read username from environment variable", "name", defaultUsernameEnvVarName)
} }
password := h.getEnv(defaultPasswordEnvVarName) password := h.getEnv(defaultPasswordEnvVarName)
@ -530,7 +529,7 @@ func (h *handlerState) getUsernameAndPassword() (string, string, error) {
return "", "", fmt.Errorf("error prompting for password: %w", err) return "", "", fmt.Errorf("error prompting for password: %w", err)
} }
} else { } else {
h.logger.V(debugLogLevel).Info("Pinniped: Read password from environment variable", "name", defaultPasswordEnvVarName) h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Read password from environment variable", "name", defaultPasswordEnvVarName)
} }
return username, password, nil return username, password, nil
@ -542,7 +541,7 @@ func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOp
// Attempt to open a local TCP listener, logging but otherwise ignoring any error. // Attempt to open a local TCP listener, logging but otherwise ignoring any error.
listener, err := h.listen("tcp", h.listenAddr) listener, err := h.listen("tcp", h.listenAddr)
if err != nil { if err != nil {
h.logger.V(debugLogLevel).Error(err, "could not open callback listener") h.logger.V(plog.KlogLevelDebug).Error(err, "could not open callback listener")
} }
// If the listener failed to start and stdin is not a TTY, then we have no hope of succeeding, // If the listener failed to start and stdin is not a TTY, then we have no hope of succeeding,
@ -578,7 +577,7 @@ func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOp
// Open the authorize URL in the users browser, logging but otherwise ignoring any error. // Open the authorize URL in the users browser, logging but otherwise ignoring any error.
if err := h.openURL(authorizeURL); err != nil { if err := h.openURL(authorizeURL); err != nil {
h.logger.V(debugLogLevel).Error(err, "could not open browser") h.logger.V(plog.KlogLevelDebug).Error(err, "could not open browser")
} }
// Prompt the user to visit the authorize URL, and to paste a manually-copied auth code (if possible). // Prompt the user to visit the authorize URL, and to paste a manually-copied auth code (if possible).
@ -709,7 +708,7 @@ func (h *handlerState) initOIDCDiscovery() error {
return err return err
} }
h.logger.V(debugLogLevel).Info("Pinniped: Performing OIDC discovery", "issuer", h.issuer) h.logger.V(plog.KlogLevelDebug).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 {
@ -767,7 +766,7 @@ func stringSliceContains(slice []string, s string) bool {
} }
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) h.logger.V(plog.KlogLevelDebug).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
@ -838,13 +837,13 @@ 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.") h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Refreshing cached token.")
upstreamOIDCIdentityProvider := h.getProvider(h.oauth2Config, h.provider, h.httpClient) upstreamOIDCIdentityProvider := h.getProvider(h.oauth2Config, h.provider, h.httpClient)
refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token) refreshed, err := upstreamOIDCIdentityProvider.PerformRefresh(ctx, refreshToken.Token)
if err != nil { if err != nil {
// Ignore errors during refresh, but return nil which will trigger the full login flow. // Ignore errors during refresh, but return nil which will trigger the full login flow.
h.logger.V(debugLogLevel).Info("Pinniped: Refresh failed.", "error", err.Error()) h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Refresh failed.", "error", err.Error())
return nil, nil return nil, nil
} }
@ -865,7 +864,7 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
if h.useFormPost { // nolint:nestif if h.useFormPost { // nolint:nestif
// Return HTTP 405 for anything that's not a POST or an OPTIONS request. // Return HTTP 405 for anything that's not a POST or an OPTIONS request.
if r.Method != http.MethodPost && r.Method != http.MethodOptions { if r.Method != http.MethodPost && r.Method != http.MethodOptions {
h.logger.V(debugLogLevel).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method) h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return nil // keep listening for more requests return nil // keep listening for more requests
} }
@ -883,11 +882,11 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
if origin == "" { if origin == "" {
// The CORS preflight request should have an origin. // The CORS preflight request should have an origin.
h.logger.V(debugLogLevel).Info("Pinniped: Got OPTIONS request without origin header") h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got OPTIONS request without origin header")
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return nil // keep listening for more requests return nil // keep listening for more requests
} }
h.logger.V(debugLogLevel).Info("Pinniped: Got CORS preflight request from browser", "origin", origin) h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got CORS preflight request from browser", "origin", origin)
// To tell the browser that it is okay to make the real POST request, return the following response. // To tell the browser that it is okay to make the real POST request, return the following response.
w.Header().Set("Access-Control-Allow-Origin", allowOrigin) w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
w.Header().Set("Vary", "*") // supposed to use Vary when Access-Control-Allow-Origin is a specific host w.Header().Set("Vary", "*") // supposed to use Vary when Access-Control-Allow-Origin is a specific host
@ -921,7 +920,7 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req
} else { } else {
// Return HTTP 405 for anything that's not a GET. // Return HTTP 405 for anything that's not a GET.
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
h.logger.V(debugLogLevel).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method) h.logger.V(plog.KlogLevelDebug).Info("Pinniped: Got unexpected request on callback listener", "method", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return nil // keep listening for more requests return nil // keep listening for more requests
} }

View File

@ -20,10 +20,6 @@ import (
"testing" "testing"
"time" "time"
"go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/testutil/tlsserver"
"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"
@ -35,9 +31,12 @@ import (
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/httputil/roundtripper" "go.pinniped.dev/internal/httputil/roundtripper"
"go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider" "go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider"
"go.pinniped.dev/internal/net/phttp"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger" "go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/internal/testutil/tlsserver"
"go.pinniped.dev/internal/upstreamoidc" "go.pinniped.dev/internal/upstreamoidc"
"go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/oidctypes"
@ -1891,7 +1890,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
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.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements testLogger := testlogger.NewLegacy(t) // nolint: staticcheck // old test with lots of log statements
klog.SetLogger(testLogger.Logger) klog.SetLogger(testLogger.Logger)
tok, err := Login(tt.issuer, tt.clientID, tok, err := Login(tt.issuer, tt.clientID,
@ -2334,7 +2333,7 @@ func TestHandleAuthCodeCallback(t *testing.T) {
state: state.State("test-state"), state: state.State("test-state"),
pkce: pkce.Code("test-pkce"), pkce: pkce.Code("test-pkce"),
nonce: nonce.Nonce("test-nonce"), nonce: nonce.Nonce("test-nonce"),
logger: testlogger.New(t).Logger, logger: plog.Logr(), // nolint: staticcheck // old test with no log assertions
issuer: "https://valid-issuer.com/with/some/path", issuer: "https://valid-issuer.com/with/some/path",
} }
if tt.opt != nil { if tt.opt != nil {

View File

@ -27,8 +27,8 @@ Create an [ActiveDirectoryIdentityProvider](https://github.com/vmware-tanzu/pinn
### ActiveDirectoryIdentityProvider with default options ### ActiveDirectoryIdentityProvider with default options
This ActiveDirectoryIdentityProvider uses all the default configuration options. This ActiveDirectoryIdentityProvider uses all the default configuration options.
The default configuration options are documented in the
Learn more about the default configuration [here]({{< ref "../reference/active-directory-configuration">}}) [Active Directory configuration reference]({{< ref "../reference/active-directory-configuration">}}).
```yaml ```yaml
apiVersion: idp.supervisor.pinniped.dev/v1alpha1 apiVersion: idp.supervisor.pinniped.dev/v1alpha1
@ -41,14 +41,13 @@ spec:
# Specify the host of the Active Directory server. # Specify the host of the Active Directory server.
host: "activedirectory.example.com:636" host: "activedirectory.example.com:636"
# Specify the name of the Kubernetes Secret that contains your Active Directory # Specify the name of the Kubernetes Secret that contains your Active
# bind account credentials. This service account will be used by the # Directory bind account credentials. This service account will be
# Supervisor to perform LDAP user and group searches. # used by the Supervisor to perform LDAP user and group searches.
bind: bind:
secretName: "active-directory-bind-account" secretName: "active-directory-bind-account"
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@ -64,6 +63,10 @@ stringData:
password: "YOUR_PASSWORD" password: "YOUR_PASSWORD"
``` ```
Note that the `metadata.name` of the ActiveDirectoryIdentityProvider resource may be visible to end users at login prompts,
so choose a name which will be understood by your end users.
For example, if you work at Acme Corp, choose something like `acme-corporate-active-directory` over `my-idp`.
If you've saved this into a file `activedirectory.yaml`, then install it into your cluster using: If you've saved this into a file `activedirectory.yaml`, then install it into your cluster using:
```sh ```sh
@ -140,13 +143,16 @@ spec:
# successful authentication. # successful authentication.
groupName: "dn" groupName: "dn"
# Specify the name of the Kubernetes Secret that contains your Active Directory # Specify the name of the Kubernetes Secret that contains your Active
# bind account credentials. This service account will be used by the # Directory bind account credentials. This service account will be
# Supervisor to perform LDAP user and group searches. # used by the Supervisor to perform LDAP user and group searches.
bind: bind:
secretName: "active-directory-bind-account" secretName: "active-directory-bind-account"
``` ```
More information about the defaults for these configuration options can be found in
the [Active Directory configuration reference]({{< ref "../reference/active-directory-configuration">}}).
## Next steps ## Next steps
Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})! Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})!

View File

@ -104,19 +104,21 @@ spec:
# to the "username" claim in downstream tokens minted by the Supervisor. # to the "username" claim in downstream tokens minted by the Supervisor.
username: email username: email
# Specify the name of the claim in your Dex ID token that represents the groups # Specify the name of the claim in your Dex ID token that represents the
# that the user belongs to. This matches what you specified above # groups to which the user belongs. This matches what you specified above
# with the Groups claim filter. # with the Groups claim filter.
# Note that the group claims from Github are in the format of "org:team". # Note that the group claims from Github are in the format of "org:team".
# To query for the group scope, you should set the organization you want Dex to # To query for the group scope, you should set the organization you
# search against in its configuration, otherwise your group claim would be empty. # want Dex to search against in its configuration, otherwise your group
# An example config can be found at - https://dexidp.io/docs/connectors/github/#configuration # claim would be empty. An example config can be found at
# https://dexidp.io/docs/connectors/github/#configuration
groups: groups groups: groups
# Specify the name of the Kubernetes Secret that contains your Dex # Specify the name of the Kubernetes Secret that contains your Dex
# application's client credentials (created below). # application's client credentials (created below).
client: client:
secretName: dex-client-credentials secretName: dex-client-credentials
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
@ -125,13 +127,19 @@ metadata:
name: dex-client-credentials name: dex-client-credentials
type: secrets.pinniped.dev/oidc-client type: secrets.pinniped.dev/oidc-client
stringData: stringData:
# The "Client ID" that you set in Dex. For example, in our case this is "pinniped-supervisor" # The "Client ID" that you set in Dex. For example, in our case
# this is "pinniped-supervisor".
clientID: "<your-client-id>" clientID: "<your-client-id>"
# The "Client secret" that you set in Dex. For example, in our case this is "pinniped-supervisor-secret" # The "Client secret" that you set in Dex. For example, in our
# case this is "pinniped-supervisor-secret".
clientSecret: "<your-client-secret>" clientSecret: "<your-client-secret>"
``` ```
Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts
if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users.
For example, if you work at Acme Corp, choose something like `acme-corporate-ldap` over `my-idp`.
Once your OIDCIdentityProvider resource has been created, you can validate your configuration by running: Once your OIDCIdentityProvider resource has been created, you can validate your configuration by running:
```bash ```bash

View File

@ -89,6 +89,7 @@ spec:
# application's client credentials (created below). # application's client credentials (created below).
client: client:
secretName: gitlab-client-credentials secretName: gitlab-client-credentials
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
@ -105,6 +106,10 @@ stringData:
clientSecret: "<your-client-secret>" clientSecret: "<your-client-secret>"
``` ```
Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts
if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users.
For example, if you work at Acme Corp, choose something like `acme-corporate-gitlab` over `my-idp`.
Once your OIDCIdentityProvider has been created, you can validate your configuration by running: Once your OIDCIdentityProvider has been created, you can validate your configuration by running:
```shell ```shell

View File

@ -120,7 +120,6 @@ spec:
secretName: "jumpcloudldap-bind-account" secretName: "jumpcloudldap-bind-account"
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@ -138,6 +137,10 @@ stringData:
password: "YOUR_PASSWORD" password: "YOUR_PASSWORD"
``` ```
Note that the `metadata.name` of the LDAPIdentityProvider resource may be visible to end users at login prompts,
so choose a name which will be understood by your end users.
For example, if you work at Acme Corp, choose something like `acme-corporate-ldap` over `my-idp`.
If you've saved this into a file `jumpcloud.yaml`, then install it into your cluster using: If you've saved this into a file `jumpcloud.yaml`, then install it into your cluster using:
```sh ```sh

View File

@ -97,6 +97,7 @@ spec:
# application's client credentials (created below). # application's client credentials (created below).
client: client:
secretName: okta-client-credentials secretName: okta-client-credentials
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
@ -113,6 +114,10 @@ stringData:
clientSecret: "<your-client-secret>" clientSecret: "<your-client-secret>"
``` ```
Note that the `metadata.name` of the OIDCIdentityProvider resource may be visible to end users at login prompts
if you choose to enable `allowPasswordGrant`, so choose a name which will be understood by your end users.
For example, if you work at Acme Corp, choose something like `acme-corporate-okta` over `my-idp`.
Once your OIDCIdentityProvider has been created, you can validate your configuration by running: Once your OIDCIdentityProvider has been created, you can validate your configuration by running:
```shell ```shell

View File

@ -158,6 +158,7 @@ spec:
- name: certs - name: certs
secret: secret:
secretName: certs secretName: certs
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -265,7 +266,6 @@ spec:
secretName: openldap-bind-account secretName: openldap-bind-account
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@ -284,6 +284,10 @@ stringData:
EOF EOF
``` ```
Note that the `metadata.name` of the LDAPIdentityProvider resource may be visible to end users at login prompts,
so choose a name which will be understood by your end users.
For example, if you work at Acme Corp, choose something like `acme-corporate-ldap` over `my-idp`.
Once your LDAPIdentityProvider has been created, you can validate your configuration by running: Once your LDAPIdentityProvider has been created, you can validate your configuration by running:
```sh ```sh

View File

@ -76,7 +76,8 @@ spec:
# the default claims in your token. The "openid" scope is always # the default claims in your token. The "openid" scope is always
# included. # included.
# #
# See the example claims below to learn how to customize the claims returned. # See the example claims below to learn how to customize the
# claims returned.
additionalScopes: [group, email] additionalScopes: [group, email]
# Specify how Workspace ONE Access claims are mapped to Kubernetes identities. # Specify how Workspace ONE Access claims are mapped to Kubernetes identities.
@ -85,22 +86,22 @@ spec:
# Specify the name of the claim in your Workspace ONE Access token that # Specify the name of the claim in your Workspace ONE Access token that
# will be mapped to the username in your Kubernetes environment. # will be mapped to the username in your Kubernetes environment.
# #
# User's emails can change. Use the sub claim if # User's emails can change. Use the sub claim if your environment
# your environment requires a stable identifier. # requires a stable identifier.
username: email username: email
# Specify the name of the claim in Workspace ONE Access that represents the # Specify the name of the claim in Workspace ONE Access that represents
# groups the user belongs to. # the groups to which the user belongs.
# #
# Group names may not be unique and can change. # Group names may not be unique and can change. The group_ids claim is
# The group_ids claim is recommended for environments # recommended for environments that want to use a more stable identifier.
# that want to use a more stable identifier.
groups: group_names groups: group_names
# Specify the name of the Kubernetes Secret that contains your # Specify the name of the Kubernetes Secret that contains your
# Workspace ONE Access application's client credentials (created below). # Workspace ONE Access application's client credentials (created below).
client: client:
secretName: ws1-client-credentials secretName: ws1-client-credentials
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret

View File

@ -244,6 +244,6 @@ should be signed by a certificate authority that is trusted by their browsers.
## Next steps ## Next steps
Next, configure an OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or an LDAPIdentityProvider for the Supervisor Next, configure an OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or an LDAPIdentityProvider for the Supervisor
(several examples are available in these guides), (several examples are available in these guides). Then
and [configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}) [configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}})
on each cluster! on each cluster!

View File

@ -72,6 +72,9 @@ pinniped get kubeconfig \
The new Pinniped-compatible kubeconfig YAML will be output as stdout, and can be redirected to a file. The new Pinniped-compatible kubeconfig YAML will be output as stdout, and can be redirected to a file.
Various default behaviors of `pinniped get kubeconfig` can be overridden using [its command-line options]({{< ref "cli" >}}). Various default behaviors of `pinniped get kubeconfig` can be overridden using [its command-line options]({{< ref "cli" >}}).
One flag of note is `--upstream-identity-provider-flow browser_authcode` to choose end-user `kubectl` login via a web browser
(the default for OIDCIdentityProviders), and `--upstream-identity-provider-flow cli_password` to choose end-user `kubectl`
login via CLI username/password prompts (the default for LDAPIdentityProviders and ActiveDirectoryIdentityProviders).
## Use the generated kubeconfig with `kubectl` to access the cluster ## Use the generated kubeconfig with `kubectl` to access the cluster
@ -94,20 +97,33 @@ to authenticate the user to the cluster.
If the Pinniped Supervisor is used for authentication to that cluster, then the user's authentication experience If the Pinniped Supervisor is used for authentication to that cluster, then the user's authentication experience
will depend on which type of identity provider was configured. will depend on which type of identity provider was configured.
- For an OIDC identity provider, there are two supported client flows. - For an OIDC identity provider, there are two supported client flows:
When using the default browser-based flow, `kubectl` will open the user's web browser and direct it to the login page of 1. When using the default browser-based flow, `kubectl` will open the user's web browser and direct it to the login page of
their OIDC Provider. This login flow is controlled by the provider, so it may include two-factor authentication or their OIDC Provider. This login flow is controlled by the provider, so it may include two-factor authentication or
other features provided by the OIDC Provider. If the user's browser is not available, then `kubectl` will instead other features provided by the OIDC Provider. If the user's browser is not available, then `kubectl` will instead
print a URL which can be visited in a browser (potentially on a different computer) to complete the authentication. print a URL which can be visited in a browser (potentially on a different computer) to complete the authentication.
When using the optional CLI-based flow, `kubectl` will interactively prompt the user for their username and password at the CLI. 2. When using the optional CLI-based flow, `kubectl` will interactively prompt the user for their username and password at the CLI.
Alternatively, the user can set the environment variables `PINNIPED_USERNAME` and `PINNIPED_PASSWORD` for the
`kubectl` process to avoid the interactive prompts. Note that the optional CLI-based flow must be enabled by the
administrator in the OIDCIdentityProvider configuration before use
(see `allowPasswordGrant` in the
[API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcauthorizationconfig)
for more details).
- For LDAP and Active Directory identity providers, there are also two supported client flows:
1. When using the default CLI-based flow, `kubectl` will interactively prompt the user for their username and password at the CLI.
Alternatively, the user can set the environment variables `PINNIPED_USERNAME` and `PINNIPED_PASSWORD` for the Alternatively, the user can set the environment variables `PINNIPED_USERNAME` and `PINNIPED_PASSWORD` for the
`kubectl` process to avoid the interactive prompts. `kubectl` process to avoid the interactive prompts.
- For an LDAP identity provider, `kubectl` will interactively prompt the user for their username and password at the CLI. 2. When using the optional browser-based flow, `kubectl` will open the user's web browser and direct it to a login page
Alternatively, the user can set the environment variables `PINNIPED_USERNAME` and `PINNIPED_PASSWORD` for the hosted by the Pinniped Supervisor. When the user enters their username and password, the Supervisor will authenticate
`kubectl` process to avoid the interactive prompts. the user using the LDAP or Active Directory provider. If the user's browser is not available, then `kubectl` will instead
print a URL which can be visited in a browser (potentially on a different computer) to complete the authentication.
Unlike the optional flow for OIDC providers described above, this optional flow does not need to be configured in
the LDAPIdentityProvider or ActiveDirectoryIdentityProvider resource, so it is always available for end-users.
Once the user completes authentication, the `kubectl` command will automatically continue and complete the user's requested command. Once the user completes authentication, the `kubectl` command will automatically continue and complete the user's requested command.
For the example above, `kubectl` would list the cluster's namespaces. For the example above, `kubectl` would list the cluster's namespaces.
@ -135,8 +151,14 @@ in the upstream identity provider, for example:
--group auditors --group auditors
``` ```
## Other notes ## Session and credential caching by the CLI
- Temporary session credentials such as ID, access, and refresh tokens are stored in: Temporary session credentials such as ID, access, and refresh tokens are stored in:
- `~/.config/pinniped/sessions.yaml` (macOS/Linux) - `$HOME/.config/pinniped/sessions.yaml` (macOS/Linux)
- `%USERPROFILE%/.config/pinniped/sessions.yaml` (Windows). - `%USERPROFILE%/.config/pinniped/sessions.yaml` (Windows).
Temporary cluster credentials such mTLS client certificates are stored in:
- `$HOME/.config/pinniped/credentials.yaml` (macOS/Linux)
- `%USERPROFILE%/.config/pinniped/credentials.yaml` (Windows).
Deleting the contents of these directories is equivalent to performing a client-side logout.

View File

@ -206,6 +206,8 @@ The per-FederationDomain endpoints are:
See [internal/oidc/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/callback/callback_handler.go). See [internal/oidc/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/callback/callback_handler.go).
- `<issuer_path>/v1alpha1/pinniped_identity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers. - `<issuer_path>/v1alpha1/pinniped_identity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers.
See [internal/oidc/idpdiscovery/idp_discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/idpdiscovery/idp_discovery_handler.go). See [internal/oidc/idpdiscovery/idp_discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/idpdiscovery/idp_discovery_handler.go).
- `<issuer_path>/login` is a login UI page to support the optional browser-based login flow for LDAP and Active Directory identity providers.
See [internal/oidc/login/login_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/oidc/login/login_handler.go).
The OIDC specifications implemented by the Supervisor can be found at [openid.net](https://openid.net/connect). The OIDC specifications implemented by the Supervisor can be found at [openid.net](https://openid.net/connect).

Some files were not shown because too many files have changed in this diff Show More