Merge branch 'main' into upstream_access_revocation_during_gc
This commit is contained in:
commit
814399324f
@ -1,9 +1,9 @@
|
|||||||
# syntax = docker/dockerfile:1.0-experimental
|
# syntax = docker/dockerfile:1.0-experimental
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
FROM golang:1.17.4 as build-env
|
FROM golang:1.17.6 as build-env
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY . .
|
COPY . .
|
||||||
@ -24,7 +24,7 @@ RUN \
|
|||||||
ln -s /usr/local/bin/pinniped-server /usr/local/bin/local-user-authenticator
|
ln -s /usr/local/bin/pinniped-server /usr/local/bin/local-user-authenticator
|
||||||
|
|
||||||
# Use a distroless runtime image with CA certificates, timezone data, and not much else.
|
# Use a distroless runtime image with CA certificates, timezone data, and not much else.
|
||||||
FROM gcr.io/distroless/static:nonroot@sha256:bca3c203cdb36f5914ab8568e4c25165643ea9b711b41a8a58b42c80a51ed609
|
FROM gcr.io/distroless/static:nonroot@sha256:80c956fb0836a17a565c43a4026c9c80b2013c83bea09f74fa4da195a59b7a99
|
||||||
|
|
||||||
# Copy the server binary from the build-env stage.
|
# Copy the server binary from the build-env stage.
|
||||||
COPY --from=build-env /usr/local/bin /usr/local/bin
|
COPY --from=build-env /usr/local/bin /usr/local/bin
|
||||||
|
14
README.md
14
README.md
@ -32,12 +32,14 @@ building and testing the code, submitting PRs, and other contributor topics.
|
|||||||
|
|
||||||
## Community meetings
|
## Community meetings
|
||||||
|
|
||||||
Pinniped is better because of our contributors and [maintainers](MAINTAINERS.md). It is because of you that we can bring great
|
Pinniped is better because of our contributors and [maintainers](MAINTAINERS.md). It is because of you that we can bring great
|
||||||
software to the community. Please join us during our online community meetings,
|
software to the community. Please join us during our online community meetings, occurring every first and third
|
||||||
occurring every first and third Thursday of the month at 9 AM PT / 12 PM ET.
|
Thursday of the month at 9 AM PT / 12 PM ET.
|
||||||
Use [this Zoom Link](https://go.pinniped.dev/community/zoom)
|
|
||||||
to attend and add any agenda items you wish to discuss
|
**Note:** Community meetings are currently paused until early 2022 as we wind down 2021!
|
||||||
to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view).
|
|
||||||
|
Use [this Zoom Link](https://go.pinniped.dev/community/zoom) to attend and add any agenda items you wish to
|
||||||
|
discuss to [the notes document](https://go.pinniped.dev/community/agenda).
|
||||||
Join our [Google Group](https://groups.google.com/g/project-pinniped) to receive invites to this meeting.
|
Join our [Google Group](https://groups.google.com/g/project-pinniped) to receive invites to this meeting.
|
||||||
|
|
||||||
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
|
If the meeting day falls on a US holiday, please consider that occurrence of the meeting to be canceled.
|
||||||
|
@ -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 v1alpha1
|
package v1alpha1
|
||||||
|
@ -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
|
||||||
@ -2850,7 +2850,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
})
|
})
|
||||||
issuerEndpointPtr = &issuerEndpoint
|
issuerEndpointPtr = &issuerEndpoint
|
||||||
|
|
||||||
testLog := testlogger.New(t)
|
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 {
|
||||||
@ -2876,7 +2876,7 @@ func TestGetKubeconfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return fake, nil
|
return fake, nil
|
||||||
},
|
},
|
||||||
log: testLog,
|
log: testLog.Logger,
|
||||||
})
|
})
|
||||||
require.NotNil(t, cmd)
|
require.NotNil(t, cmd)
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -358,8 +358,8 @@ func TestLoginOIDCCommand(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.New(t)
|
testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
klog.SetLogger(testLogger)
|
klog.SetLogger(testLogger.Logger)
|
||||||
var (
|
var (
|
||||||
gotOptions []oidcclient.Option
|
gotOptions []oidcclient.Option
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
@ -165,8 +165,8 @@ 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.New(t)
|
testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
klog.SetLogger(testLogger)
|
klog.SetLogger(testLogger.Logger)
|
||||||
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]
|
||||||
|
@ -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
|
||||||
@ -7,8 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/plog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint: gochecknoglobals
|
//nolint: gochecknoglobals
|
||||||
@ -19,12 +17,6 @@ var rootCmd = &cobra.Command{
|
|||||||
SilenceUsage: true, // do not print usage message when commands fail
|
SilenceUsage: true, // do not print usage message when commands fail
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint: gochecknoinits
|
|
||||||
func init() {
|
|
||||||
// We don't want klog flags showing up in our CLI.
|
|
||||||
plog.RemoveKlogGlobalFlags()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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() {
|
||||||
|
@ -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 v1alpha1
|
package v1alpha1
|
||||||
|
@ -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 v1alpha1
|
package v1alpha1
|
||||||
|
@ -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 v1alpha1
|
package v1alpha1
|
||||||
|
@ -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 v1alpha1
|
package v1alpha1
|
||||||
|
@ -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 v1alpha1
|
package v1alpha1
|
||||||
|
197
go.mod
197
go.mod
@ -2,152 +2,189 @@ module go.pinniped.dev
|
|||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
|
// Unfortuntely, having any indirect dependency on github.com/oleiade/reflections@v1.0.0
|
||||||
|
// seems to cause Dependabot to stop scanning our dependencies due to a checksum error for the package.
|
||||||
|
// The cause of the checksum error is described in https://github.com/oleiade/reflections/issues/14.
|
||||||
|
//
|
||||||
|
// According to `go mod graph`, this dependency is (currently) coming from:
|
||||||
|
// go.pinniped.dev -> github.com/ory/x@v0.0.212 -> github.com/ory/analytics-go/v4@v4.0.0 -> github.com/ory/x@v0.0.110 -> github.com/ory/fosite@v0.29.0 -> github.com/oleiade/reflections@v1.0.0
|
||||||
|
// So the issue is that older versions of ory/x had a direct dependency on an old version of Fosite.
|
||||||
|
// Newer versions of ory/x do not depend on fosite anymore. We can use a replace directive until none
|
||||||
|
// of our indirect dependencies pull in any old versions of ory/x anymore.
|
||||||
|
//
|
||||||
|
// Whenever we upgrade fosite and ory/x, we can try removing this replace directive and running
|
||||||
|
// `go mod tidy` to see if github.com/oleiade/reflections@v1.0.0 still appears in our go.sum.
|
||||||
|
// As long as it does, we probably need to keep this replace directive.
|
||||||
|
replace github.com/oleiade/reflections => github.com/oleiade/reflections v1.0.1
|
||||||
|
|
||||||
|
// bumping github.com/ory/x to higher than v0.0.297 breaks k8s.io/apiserver via go.opentelemetry.io/otel/semconv
|
||||||
|
// force the use of an old version for now as it seems to allow a newer ory/x without breaking the apiserver lib.
|
||||||
|
// all go.opentelemetry.io replace directives are copied from:
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/3bce0502aac87f9907af0ef19df5935632ceafdf/go.mod#L432-L443
|
||||||
|
replace (
|
||||||
|
go.opentelemetry.io/contrib => go.opentelemetry.io/contrib v0.20.0
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc => go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0
|
||||||
|
go.opentelemetry.io/otel => go.opentelemetry.io/otel v0.20.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp => go.opentelemetry.io/otel/exporters/otlp v0.20.0
|
||||||
|
go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v0.20.0
|
||||||
|
go.opentelemetry.io/otel/oteltest => go.opentelemetry.io/otel/oteltest v0.20.0
|
||||||
|
go.opentelemetry.io/otel/sdk => go.opentelemetry.io/otel/sdk v0.20.0
|
||||||
|
go.opentelemetry.io/otel/sdk/export/metric => go.opentelemetry.io/otel/sdk/export/metric v0.20.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric => go.opentelemetry.io/otel/sdk/metric v0.20.0
|
||||||
|
go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v0.20.0
|
||||||
|
go.opentelemetry.io/proto/otlp => go.opentelemetry.io/proto/otlp v0.7.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||||
github.com/coreos/go-oidc/v3 v3.0.0
|
github.com/coreos/go-oidc/v3 v3.1.0
|
||||||
github.com/creack/pty v1.1.14
|
github.com/creack/pty v1.1.17
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
|
github.com/felixge/httpsnoop v1.0.2
|
||||||
github.com/go-ldap/ldap/v3 v3.4.1
|
github.com/go-ldap/ldap/v3 v3.4.1
|
||||||
github.com/go-logr/logr v0.4.0
|
github.com/go-logr/logr v1.2.2
|
||||||
github.com/go-logr/stdr v0.4.0
|
github.com/go-logr/stdr v1.2.2
|
||||||
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.6
|
github.com/google/go-cmp v0.5.6
|
||||||
github.com/google/gofuzz v1.2.0
|
github.com/google/gofuzz v1.2.0
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/securecookie v1.1.1
|
github.com/gorilla/securecookie v1.1.1
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||||
github.com/ory/fosite v0.40.2
|
github.com/ory/fosite v0.41.0
|
||||||
github.com/ory/x v0.0.212
|
github.com/ory/x v0.0.331
|
||||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/sclevine/agouti v3.0.0+incompatible
|
github.com/sclevine/agouti v3.0.0+incompatible
|
||||||
github.com/sclevine/spec v1.4.0
|
github.com/sclevine/spec v1.4.0
|
||||||
github.com/spf13/cobra v1.2.1
|
github.com/spf13/cobra v1.3.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/tdewolff/minify/v2 v2.9.21
|
github.com/tdewolff/minify/v2 v2.9.26
|
||||||
go.uber.org/atomic v1.7.0
|
go.uber.org/atomic v1.9.0
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023
|
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f
|
||||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||||
|
golang.org/x/text v0.3.7
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
k8s.io/api v0.22.2
|
k8s.io/api v0.23.1
|
||||||
k8s.io/apiextensions-apiserver v0.22.2
|
k8s.io/apiextensions-apiserver v0.23.1
|
||||||
k8s.io/apimachinery v0.22.2
|
k8s.io/apimachinery v0.23.1
|
||||||
k8s.io/apiserver v0.22.2
|
k8s.io/apiserver v0.23.1
|
||||||
k8s.io/client-go v0.22.2
|
k8s.io/client-go v0.23.1
|
||||||
k8s.io/component-base v0.22.2
|
k8s.io/component-base v0.23.1
|
||||||
k8s.io/gengo v0.0.0-20210203185629-de9496dff47b
|
k8s.io/gengo v0.0.0-20211129171323-c02415ce4185
|
||||||
k8s.io/klog/v2 v2.10.0
|
k8s.io/klog/v2 v2.40.1
|
||||||
k8s.io/kube-aggregator v0.22.1
|
k8s.io/kube-aggregator v0.23.1
|
||||||
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a
|
k8s.io/utils v0.0.0-20211208161948-7d6a63dca704
|
||||||
sigs.k8s.io/yaml v1.2.0
|
sigs.k8s.io/yaml v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.81.0 // indirect
|
cloud.google.com/go v0.99.0 // indirect
|
||||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||||
github.com/Azure/go-autorest/autorest v0.11.18 // indirect
|
github.com/Azure/go-autorest/autorest v0.11.23 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
|
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
|
||||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect
|
||||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
github.com/coreos/go-oidc v2.2.1+incompatible // indirect
|
||||||
github.com/coreos/go-oidc v2.1.0+incompatible // indirect
|
|
||||||
github.com/coreos/go-semver v0.3.0 // indirect
|
github.com/coreos/go-semver v0.3.0 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||||
github.com/dgraph-io/ristretto v0.0.3 // indirect
|
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||||
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
|
github.com/emicklei/go-restful v2.15.0+incompatible // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
|
||||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
github.com/go-openapi/swag v0.19.14 // indirect
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||||
|
github.com/golang/glog v1.0.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.5 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.11 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/magiconair/properties v1.8.5 // indirect
|
github.com/magiconair/properties v1.8.5 // indirect
|
||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/goveralls v0.0.6 // indirect
|
github.com/mattn/goveralls v0.0.11 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||||
github.com/moby/spdystream v0.2.0 // indirect
|
github.com/moby/spdystream v0.2.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ory/go-acc v0.2.6 // indirect
|
github.com/ory/go-acc v0.2.6 // indirect
|
||||||
github.com/ory/go-convenience v0.1.0 // indirect
|
github.com/ory/go-convenience v0.1.0 // indirect
|
||||||
github.com/ory/viper v1.7.5 // indirect
|
github.com/ory/viper v1.7.5 // indirect
|
||||||
github.com/pborman/uuid v1.2.0 // indirect
|
github.com/pborman/uuid v1.2.1 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 // indirect
|
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
github.com/prometheus/common v0.26.0 // indirect
|
github.com/prometheus/common v0.32.1 // indirect
|
||||||
github.com/prometheus/procfs v0.6.0 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
github.com/spf13/afero v1.7.1 // indirect
|
||||||
github.com/spf13/afero v1.6.0 // indirect
|
github.com/spf13/cast v1.4.1 // indirect
|
||||||
github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8 // indirect
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/subosito/gotenv v1.2.0 // indirect
|
github.com/subosito/gotenv v1.2.0 // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.5.19 // indirect
|
github.com/tdewolff/parse/v2 v2.5.26 // indirect
|
||||||
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
|
go.etcd.io/etcd/api/v3 v3.5.1 // indirect
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
|
go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect
|
||||||
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
|
go.etcd.io/etcd/client/v3 v3.5.1 // indirect
|
||||||
go.opentelemetry.io/contrib v0.20.0 // indirect
|
go.opentelemetry.io/contrib v0.20.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect
|
||||||
go.opentelemetry.io/otel v0.20.0 // indirect
|
go.opentelemetry.io/otel v1.2.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.2.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v0.20.0 // indirect
|
go.opentelemetry.io/otel/trace v1.2.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
|
go.opentelemetry.io/proto/otlp v0.10.0 // indirect
|
||||||
go.uber.org/multierr v1.6.0 // indirect
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
go.uber.org/zap v1.17.0 // indirect
|
go.uber.org/zap v1.19.1 // indirect
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
|
golang.org/x/mod v0.5.1 // indirect
|
||||||
golang.org/x/text v0.3.6 // indirect
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||||
golang.org/x/tools v0.1.2 // indirect
|
golang.org/x/tools v0.1.8 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
|
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect
|
||||||
google.golang.org/grpc v1.38.0 // indirect
|
google.golang.org/grpc v1.43.0 // indirect
|
||||||
google.golang.org/protobuf v1.26.0 // indirect
|
google.golang.org/protobuf v1.27.1 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
|
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
|
||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22 // indirect
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.27 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
|
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
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
|
||||||
|
@ -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 authenticators contains authenticator interfaces.
|
// Package authenticators contains authenticator interfaces.
|
||||||
@ -35,6 +35,7 @@ type UserAuthenticator interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
User user.Info
|
User user.Info
|
||||||
DN string
|
DN string
|
||||||
|
ExtraRefreshAttributes map[string]string
|
||||||
}
|
}
|
||||||
|
@ -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 impersonator
|
package impersonator
|
||||||
@ -31,6 +31,7 @@ import (
|
|||||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||||
|
"k8s.io/apiserver/pkg/audit"
|
||||||
"k8s.io/apiserver/pkg/audit/policy"
|
"k8s.io/apiserver/pkg/audit/policy"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||||
@ -211,7 +212,7 @@ func newInternal( //nolint:funlen // yeah, it's kind of long.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wire up a fake audit backend at the metadata level so we can preserve the original user during nested impersonation
|
// wire up a fake audit backend at the metadata level so we can preserve the original user during nested impersonation
|
||||||
serverConfig.AuditPolicyChecker = policy.FakeChecker(auditinternal.LevelMetadata, nil)
|
serverConfig.AuditPolicyRuleEvaluator = policy.NewFakePolicyRuleEvaluator(auditinternal.LevelMetadata, nil)
|
||||||
serverConfig.AuditBackend = &auditfake.Backend{}
|
serverConfig.AuditBackend = &auditfake.Backend{}
|
||||||
|
|
||||||
// Probe the API server to figure out if anonymous auth is enabled.
|
// Probe the API server to figure out if anonymous auth is enabled.
|
||||||
@ -511,7 +512,7 @@ func newImpersonationReverseProxyFunc(restConfig *rest.Config) (func(*genericapi
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ae := request.AuditEventFrom(r.Context())
|
ae := audit.AuditEventFrom(r.Context())
|
||||||
if ae == nil {
|
if ae == nil {
|
||||||
plog.Warning("aggregated API server logic did not set audit event but it is always supposed to do so",
|
plog.Warning("aggregated API server logic did not set audit event but it is always supposed to do so",
|
||||||
"url", r.URL.String(),
|
"url", r.URL.String(),
|
||||||
|
@ -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 impersonator
|
package impersonator
|
||||||
@ -27,6 +27,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||||
|
"k8s.io/apiserver/pkg/audit"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
@ -712,8 +713,8 @@ func TestImpersonator(t *testing.T) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "/apis/flowcontrol.apiserver.k8s.io/v1beta1/prioritylevelconfigurations",
|
case "/apis/flowcontrol.apiserver.k8s.io/v1beta2/prioritylevelconfigurations",
|
||||||
"/apis/flowcontrol.apiserver.k8s.io/v1beta1/flowschemas":
|
"/apis/flowcontrol.apiserver.k8s.io/v1beta2/flowschemas":
|
||||||
// ignore requests related to priority and fairness logic
|
// ignore requests related to priority and fairness logic
|
||||||
require.Equal(t, http.MethodGet, r.Method)
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@ -1125,7 +1126,7 @@ func TestImpersonatorHTTPHandler(t *testing.T) {
|
|||||||
Groups: testGroups,
|
Groups: testGroups,
|
||||||
Extra: testExtra,
|
Extra: testExtra,
|
||||||
}, nil, "")
|
}, nil, "")
|
||||||
ctx := request.WithAuditEvent(req.Context(), nil)
|
ctx := audit.WithAuditContext(req.Context(), nil)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
return req
|
return req
|
||||||
}(),
|
}(),
|
||||||
@ -1880,7 +1881,7 @@ func newRequest(t *testing.T, h http.Header, userInfo user.Info, event *auditint
|
|||||||
if event != nil {
|
if event != nil {
|
||||||
ae = event
|
ae = event
|
||||||
}
|
}
|
||||||
ctx = request.WithAuditEvent(ctx, ae)
|
ctx = audit.WithAuditContext(ctx, &audit.AuditContext{Event: ae})
|
||||||
|
|
||||||
reqInfo := &request.RequestInfo{
|
reqInfo := &request.RequestInfo{
|
||||||
IsResourceRequest: false,
|
IsResourceRequest: false,
|
||||||
|
@ -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 server is the command line entry point for pinniped-concierge.
|
// Package server is the command line entry point for pinniped-concierge.
|
||||||
@ -35,7 +35,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,8 +95,6 @@ func addCommandlineFlagsToCommand(cmd *cobra.Command, app *App) {
|
|||||||
"/etc/podinfo",
|
"/etc/podinfo",
|
||||||
"path to Downward API volume mount",
|
"path to Downward API volume mount",
|
||||||
)
|
)
|
||||||
|
|
||||||
plog.RemoveKlogGlobalFlags()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boot the aggregated API server, which will in turn boot the controllers.
|
// Boot the aggregated API server, which will in turn boot the controllers.
|
||||||
|
@ -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 apicerts
|
package apicerts
|
||||||
@ -251,13 +251,10 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
opts := &[]metav1.DeleteOptions{}
|
|
||||||
trackDeleteClient := testutil.NewDeleteOptionsRecorder(kubeAPIClient, opts)
|
|
||||||
|
|
||||||
c := NewCertsExpirerController(
|
c := NewCertsExpirerController(
|
||||||
namespace,
|
namespace,
|
||||||
certsSecretResourceName,
|
certsSecretResourceName,
|
||||||
trackDeleteClient,
|
kubeAPIClient,
|
||||||
kubeInformers.Core().V1().Secrets(),
|
kubeInformers.Core().V1().Secrets(),
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
test.renewBefore,
|
test.renewBefore,
|
||||||
@ -281,7 +278,7 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
if test.wantDelete {
|
if test.wantDelete {
|
||||||
exActions = append(
|
exActions = append(
|
||||||
exActions,
|
exActions,
|
||||||
kubetesting.NewDeleteAction(
|
kubetesting.NewDeleteActionWithOptions(
|
||||||
schema.GroupVersionResource{
|
schema.GroupVersionResource{
|
||||||
Group: "",
|
Group: "",
|
||||||
Version: "v1",
|
Version: "v1",
|
||||||
@ -289,18 +286,12 @@ func TestExpirerControllerSync(t *testing.T) {
|
|||||||
},
|
},
|
||||||
namespace,
|
namespace,
|
||||||
name,
|
name,
|
||||||
|
testutil.NewPreconditions(testUID, testRV),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
acActions := kubeAPIClient.Actions()
|
acActions := kubeAPIClient.Actions()
|
||||||
require.Equal(t, exActions, acActions)
|
require.Equal(t, exActions, acActions)
|
||||||
|
|
||||||
if test.wantDelete {
|
|
||||||
require.Len(t, *opts, 1)
|
|
||||||
require.Equal(t, testutil.NewPreconditions(testUID, testRV), (*opts)[0])
|
|
||||||
} else {
|
|
||||||
require.Len(t, *opts, 0)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 cachecleaner
|
package cachecleaner
|
||||||
@ -143,11 +143,11 @@ func TestController(t *testing.T) {
|
|||||||
if tt.initialCache != nil {
|
if tt.initialCache != nil {
|
||||||
tt.initialCache(t, cache)
|
tt.initialCache(t, cache)
|
||||||
}
|
}
|
||||||
testLog := testlogger.New(t)
|
testLog := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
|
|
||||||
webhooks := informers.Authentication().V1alpha1().WebhookAuthenticators()
|
webhooks := informers.Authentication().V1alpha1().WebhookAuthenticators()
|
||||||
jwtAuthenticators := informers.Authentication().V1alpha1().JWTAuthenticators()
|
jwtAuthenticators := informers.Authentication().V1alpha1().JWTAuthenticators()
|
||||||
controller := New(cache, webhooks, jwtAuthenticators, testLog)
|
controller := New(cache, webhooks, jwtAuthenticators, testLog.Logger)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -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 jwtcachefiller
|
package jwtcachefiller
|
||||||
@ -318,13 +318,13 @@ func TestController(t *testing.T) {
|
|||||||
fakeClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
|
fakeClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
|
||||||
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
|
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
|
||||||
cache := authncache.New()
|
cache := authncache.New()
|
||||||
testLog := testlogger.New(t)
|
testLog := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
|
|
||||||
if tt.cache != nil {
|
if tt.cache != nil {
|
||||||
tt.cache(t, cache, tt.wantClose)
|
tt.cache(t, cache, tt.wantClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog)
|
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog.Logger)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -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 webhookcachefiller
|
package webhookcachefiller
|
||||||
@ -88,9 +88,9 @@ func TestController(t *testing.T) {
|
|||||||
fakeClient := pinnipedfake.NewSimpleClientset(tt.webhooks...)
|
fakeClient := pinnipedfake.NewSimpleClientset(tt.webhooks...)
|
||||||
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
|
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
|
||||||
cache := authncache.New()
|
cache := authncache.New()
|
||||||
testLog := testlogger.New(t)
|
testLog := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
|
|
||||||
controller := New(cache, informers.Authentication().V1alpha1().WebhookAuthenticators(), testLog)
|
controller := New(cache, informers.Authentication().V1alpha1().WebhookAuthenticators(), testLog.Logger)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -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 impersonatorconfig
|
package impersonatorconfig
|
||||||
@ -21,7 +21,6 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/api/equality"
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
"k8s.io/apimachinery/pkg/util/errors"
|
"k8s.io/apimachinery/pkg/util/errors"
|
||||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
@ -31,6 +30,7 @@ import (
|
|||||||
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"
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
|
@ -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 impersonatorconfig
|
package impersonatorconfig
|
||||||
@ -29,12 +29,11 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"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/util/clock"
|
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
kubeinformers "k8s.io/client-go/informers"
|
kubeinformers "k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
|
pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
|
||||||
@ -95,7 +94,7 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
caSignerName,
|
caSignerName,
|
||||||
nil,
|
nil,
|
||||||
testLog,
|
testLog.Logger,
|
||||||
)
|
)
|
||||||
credIssuerInformerFilter = observableWithInformerOption.GetFilterForInformer(credIssuerInformer)
|
credIssuerInformerFilter = observableWithInformerOption.GetFilterForInformer(credIssuerInformer)
|
||||||
servicesInformerFilter = observableWithInformerOption.GetFilterForInformer(servicesInformer)
|
servicesInformerFilter = observableWithInformerOption.GetFilterForInformer(servicesInformer)
|
||||||
@ -270,8 +269,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
var subject controllerlib.Controller
|
var subject controllerlib.Controller
|
||||||
var kubeAPIClient *kubernetesfake.Clientset
|
var kubeAPIClient *kubernetesfake.Clientset
|
||||||
var deleteOptions *[]metav1.DeleteOptions
|
|
||||||
var deleteOptionsRecorder kubernetes.Interface
|
|
||||||
var pinnipedAPIClient *pinnipedfake.Clientset
|
var pinnipedAPIClient *pinnipedfake.Clientset
|
||||||
var pinnipedInformerClient *pinnipedfake.Clientset
|
var pinnipedInformerClient *pinnipedfake.Clientset
|
||||||
var pinnipedInformers pinnipedinformers.SharedInformerFactory
|
var pinnipedInformers pinnipedinformers.SharedInformerFactory
|
||||||
@ -550,7 +547,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
|||||||
subject = NewImpersonatorConfigController(
|
subject = NewImpersonatorConfigController(
|
||||||
installedInNamespace,
|
installedInNamespace,
|
||||||
credentialIssuerResourceName,
|
credentialIssuerResourceName,
|
||||||
deleteOptionsRecorder,
|
kubeAPIClient,
|
||||||
pinnipedAPIClient,
|
pinnipedAPIClient,
|
||||||
pinnipedInformers.Config().V1alpha1().CredentialIssuers(),
|
pinnipedInformers.Config().V1alpha1().CredentialIssuers(),
|
||||||
kubeInformers.Core().V1().Services(),
|
kubeInformers.Core().V1().Services(),
|
||||||
@ -562,11 +559,11 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
|||||||
tlsSecretName,
|
tlsSecretName,
|
||||||
caSecretName,
|
caSecretName,
|
||||||
labels,
|
labels,
|
||||||
clock.NewFakeClock(frozenNow),
|
clocktesting.NewFakeClock(frozenNow),
|
||||||
impersonatorFunc,
|
impersonatorFunc,
|
||||||
caSignerName,
|
caSignerName,
|
||||||
signingCertProvider,
|
signingCertProvider,
|
||||||
testLog,
|
testLog.Logger,
|
||||||
)
|
)
|
||||||
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
|
||||||
@ -1032,10 +1029,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
|||||||
r.Equal("secrets", deleteAction.GetResource().Resource)
|
r.Equal("secrets", deleteAction.GetResource().Resource)
|
||||||
|
|
||||||
// validate that we set delete preconditions correctly
|
// validate that we set delete preconditions correctly
|
||||||
r.NotEmpty(*deleteOptions)
|
r.Equal(testutil.NewPreconditions("uid-1234", "rv-5678"), deleteAction.GetDeleteOptions())
|
||||||
for _, opt := range *deleteOptions {
|
|
||||||
r.Equal(testutil.NewPreconditions("uid-1234", "rv-5678"), opt)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var requireCASecretWasCreated = func(action coretesting.Action) []byte {
|
var requireCASecretWasCreated = func(action coretesting.Action) []byte {
|
||||||
@ -1114,8 +1108,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) {
|
|||||||
kubeinformers.WithNamespace(installedInNamespace),
|
kubeinformers.WithNamespace(installedInNamespace),
|
||||||
)
|
)
|
||||||
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
kubeAPIClient = kubernetesfake.NewSimpleClientset()
|
||||||
deleteOptions = &[]metav1.DeleteOptions{}
|
|
||||||
deleteOptionsRecorder = testutil.NewDeleteOptionsRecorder(kubeAPIClient, deleteOptions)
|
|
||||||
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
pinnipedAPIClient = pinnipedfake.NewSimpleClientset()
|
||||||
frozenNow = time.Date(2021, time.March, 2, 7, 42, 0, 0, time.Local)
|
frozenNow = time.Date(2021, time.March, 2, 7, 42, 0, 0, time.Local)
|
||||||
signingCertProvider = dynamiccert.NewCA(name)
|
signingCertProvider = dynamiccert.NewCA(name)
|
||||||
|
@ -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 kubecertagent provides controllers that ensure a pod (the kube-cert-agent), is
|
// Package kubecertagent provides controllers that ensure a pod (the kube-cert-agent), is
|
||||||
@ -23,13 +23,13 @@ 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/clock"
|
|
||||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
appsv1informers "k8s.io/client-go/informers/apps/v1"
|
appsv1informers "k8s.io/client-go/informers/apps/v1"
|
||||||
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/klog/v2/klogr"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
|
@ -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 kubecertagent
|
package kubecertagent
|
||||||
@ -20,11 +20,11 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/cache"
|
"k8s.io/apimachinery/pkg/util/cache"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
|
||||||
@ -1027,17 +1027,14 @@ func TestAgentController(t *testing.T) {
|
|||||||
tt.addKubeReactions(kubeClientset)
|
tt.addKubeReactions(kubeClientset)
|
||||||
}
|
}
|
||||||
|
|
||||||
actualDeleteActionOpts := &[]metav1.DeleteOptions{}
|
|
||||||
trackDeleteKubeClient := testutil.NewDeleteOptionsRecorder(kubeClientset, actualDeleteActionOpts)
|
|
||||||
|
|
||||||
kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0)
|
kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0)
|
||||||
log := testlogger.New(t)
|
log := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
mockExecutor := mocks.NewMockPodCommandExecutor(ctrl)
|
mockExecutor := mocks.NewMockPodCommandExecutor(ctrl)
|
||||||
mockDynamicCert := mocks.NewMockDynamicCertPrivate(ctrl)
|
mockDynamicCert := mocks.NewMockDynamicCertPrivate(ctrl)
|
||||||
fakeClock := clock.NewFakeClock(now)
|
fakeClock := clocktesting.NewFakeClock(now)
|
||||||
execCache := cache.NewExpiringWithClock(fakeClock)
|
execCache := cache.NewExpiringWithClock(fakeClock)
|
||||||
if tt.mocks != nil {
|
if tt.mocks != nil {
|
||||||
tt.mocks(t, mockExecutor.EXPECT(), mockDynamicCert.EXPECT(), execCache)
|
tt.mocks(t, mockExecutor.EXPECT(), mockDynamicCert.EXPECT(), execCache)
|
||||||
@ -1059,7 +1056,7 @@ func TestAgentController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
DiscoveryURLOverride: tt.discoveryURLOverride,
|
DiscoveryURLOverride: tt.discoveryURLOverride,
|
||||||
},
|
},
|
||||||
&kubeclient.Client{Kubernetes: trackDeleteKubeClient, PinnipedConcierge: conciergeClientset},
|
&kubeclient.Client{Kubernetes: kubeClientset, PinnipedConcierge: conciergeClientset},
|
||||||
kubeInformers.Core().V1().Pods(),
|
kubeInformers.Core().V1().Pods(),
|
||||||
kubeInformers.Apps().V1().Deployments(),
|
kubeInformers.Apps().V1().Deployments(),
|
||||||
kubeInformers.Core().V1().Pods(),
|
kubeInformers.Core().V1().Pods(),
|
||||||
@ -1069,13 +1066,13 @@ func TestAgentController(t *testing.T) {
|
|||||||
mockDynamicCert,
|
mockDynamicCert,
|
||||||
fakeClock,
|
fakeClock,
|
||||||
execCache,
|
execCache,
|
||||||
log,
|
log.Logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
errorMessages := runControllerUntilQuiet(ctx, t, controller, hasDeploymentSynced(trackDeleteKubeClient, kubeInformers), kubeInformers, conciergeInformers)
|
errorMessages := runControllerUntilQuiet(ctx, t, controller, hasDeploymentSynced(kubeClientset, kubeInformers), kubeInformers, conciergeInformers)
|
||||||
|
|
||||||
actualErrors := deduplicate(errorMessages)
|
actualErrors := deduplicate(errorMessages)
|
||||||
assert.Subsetf(t, actualErrors, tt.wantDistinctErrors, "required error(s) were not found in the actual errors")
|
assert.Subsetf(t, actualErrors, tt.wantDistinctErrors, "required error(s) were not found in the actual errors")
|
||||||
@ -1088,16 +1085,20 @@ func TestAgentController(t *testing.T) {
|
|||||||
|
|
||||||
// Assert on all actions that happened to deployments.
|
// Assert on all actions that happened to deployments.
|
||||||
var actualDeploymentActionVerbs []string
|
var actualDeploymentActionVerbs []string
|
||||||
|
var actualDeleteActionOpts []metav1.DeleteOptions
|
||||||
for _, a := range kubeClientset.Actions() {
|
for _, a := range kubeClientset.Actions() {
|
||||||
if a.GetResource().Resource == "deployments" && a.GetVerb() != "get" { // ignore gets caused by hasDeploymentSynced
|
if a.GetResource().Resource == "deployments" && a.GetVerb() != "get" { // ignore gets caused by hasDeploymentSynced
|
||||||
actualDeploymentActionVerbs = append(actualDeploymentActionVerbs, a.GetVerb())
|
actualDeploymentActionVerbs = append(actualDeploymentActionVerbs, a.GetVerb())
|
||||||
|
if deleteAction, ok := a.(coretesting.DeleteAction); ok {
|
||||||
|
actualDeleteActionOpts = append(actualDeleteActionOpts, deleteAction.GetDeleteOptions())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tt.wantDeploymentActionVerbs != nil {
|
if tt.wantDeploymentActionVerbs != nil {
|
||||||
assert.Equal(t, tt.wantDeploymentActionVerbs, actualDeploymentActionVerbs)
|
assert.Equal(t, tt.wantDeploymentActionVerbs, actualDeploymentActionVerbs)
|
||||||
}
|
}
|
||||||
if tt.wantDeploymentDeleteActionOpts != nil {
|
if tt.wantDeploymentDeleteActionOpts != nil {
|
||||||
assert.Equal(t, tt.wantDeploymentDeleteActionOpts, *actualDeleteActionOpts)
|
assert.Equal(t, tt.wantDeploymentDeleteActionOpts, actualDeleteActionOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert that the agent deployment is in the expected final state.
|
// Assert that the agent deployment is in the expected final state.
|
||||||
|
@ -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 kubecertagent
|
package kubecertagent
|
||||||
@ -58,12 +58,10 @@ func TestLegacyPodCleanerController(t *testing.T) {
|
|||||||
wantDistinctErrors []string
|
wantDistinctErrors []string
|
||||||
wantDistinctLogs []string
|
wantDistinctLogs []string
|
||||||
wantActions []coretesting.Action
|
wantActions []coretesting.Action
|
||||||
wantDeleteOptions []metav1.DeleteOptions
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no pods",
|
name: "no pods",
|
||||||
wantActions: []coretesting.Action{},
|
wantActions: []coretesting.Action{},
|
||||||
wantDeleteOptions: []metav1.DeleteOptions{},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mix of pods",
|
name: "mix of pods",
|
||||||
@ -78,12 +76,9 @@ func TestLegacyPodCleanerController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantActions: []coretesting.Action{ // the first delete triggers the informer again, but the second invocation triggers a Not Found
|
wantActions: []coretesting.Action{ // the first delete triggers the informer again, but the second invocation triggers a Not Found
|
||||||
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
||||||
coretesting.NewDeleteAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewDeleteActionWithOptions(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name, testutil.NewPreconditions("3", "4")),
|
||||||
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
||||||
},
|
},
|
||||||
wantDeleteOptions: []metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("3", "4"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fail to delete",
|
name: "fail to delete",
|
||||||
@ -102,13 +97,9 @@ func TestLegacyPodCleanerController(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
||||||
coretesting.NewDeleteAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewDeleteActionWithOptions(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name, testutil.NewPreconditions("3", "4")),
|
||||||
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
||||||
coretesting.NewDeleteAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewDeleteActionWithOptions(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name, testutil.NewPreconditions("3", "4")),
|
||||||
},
|
|
||||||
wantDeleteOptions: []metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("3", "4"),
|
|
||||||
testutil.NewPreconditions("3", "4"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -126,10 +117,7 @@ func TestLegacyPodCleanerController(t *testing.T) {
|
|||||||
wantDistinctErrors: []string{""},
|
wantDistinctErrors: []string{""},
|
||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
||||||
coretesting.NewDeleteAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewDeleteActionWithOptions(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name, testutil.NewPreconditions("3", "4")),
|
||||||
},
|
|
||||||
wantDeleteOptions: []metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("3", "4"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -148,7 +136,6 @@ func TestLegacyPodCleanerController(t *testing.T) {
|
|||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
coretesting.NewGetAction(corev1.Resource("pods").WithVersion("v1"), "concierge", legacyAgentPodWithExtraLabel.Name),
|
||||||
},
|
},
|
||||||
wantDeleteOptions: []metav1.DeleteOptions{},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -161,19 +148,16 @@ func TestLegacyPodCleanerController(t *testing.T) {
|
|||||||
tt.addKubeReactions(kubeClientset)
|
tt.addKubeReactions(kubeClientset)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &[]metav1.DeleteOptions{}
|
|
||||||
trackDeleteClient := testutil.NewDeleteOptionsRecorder(kubeClientset, opts)
|
|
||||||
|
|
||||||
kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0)
|
kubeInformers := informers.NewSharedInformerFactory(kubeClientset, 0)
|
||||||
log := testlogger.New(t)
|
log := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
controller := NewLegacyPodCleanerController(
|
controller := NewLegacyPodCleanerController(
|
||||||
AgentConfig{
|
AgentConfig{
|
||||||
Namespace: "concierge",
|
Namespace: "concierge",
|
||||||
Labels: map[string]string{"extralabel": "labelvalue"},
|
Labels: map[string]string{"extralabel": "labelvalue"},
|
||||||
},
|
},
|
||||||
&kubeclient.Client{Kubernetes: trackDeleteClient},
|
&kubeclient.Client{Kubernetes: kubeClientset},
|
||||||
kubeInformers.Core().V1().Pods(),
|
kubeInformers.Core().V1().Pods(),
|
||||||
log,
|
log.Logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
@ -183,7 +167,6 @@ func TestLegacyPodCleanerController(t *testing.T) {
|
|||||||
assert.Equal(t, tt.wantDistinctErrors, deduplicate(errorMessages), "unexpected errors")
|
assert.Equal(t, tt.wantDistinctErrors, deduplicate(errorMessages), "unexpected errors")
|
||||||
assert.Equal(t, tt.wantDistinctLogs, deduplicate(log.Lines()), "unexpected logs")
|
assert.Equal(t, tt.wantDistinctLogs, deduplicate(log.Lines()), "unexpected logs")
|
||||||
assert.Equal(t, tt.wantActions, kubeClientset.Actions()[2:], "unexpected actions")
|
assert.Equal(t, tt.wantActions, kubeClientset.Actions()[2:], "unexpected actions")
|
||||||
assert.Equal(t, tt.wantDeleteOptions, *opts, "unexpected delete options")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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 activedirectoryupstreamwatcher implements a controller which watches ActiveDirectoryIdentityProviders.
|
// Package activedirectoryupstreamwatcher implements a controller which watches ActiveDirectoryIdentityProviders.
|
||||||
@ -7,8 +7,12 @@ package activedirectoryupstreamwatcher
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"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"
|
||||||
@ -52,6 +56,21 @@ const (
|
|||||||
// - has a member that matches the DN of the user we successfully logged in as.
|
// - has a member that matches the DN of the user we successfully logged in as.
|
||||||
// - perform nested group search by default.
|
// - perform nested group search by default.
|
||||||
defaultActiveDirectoryGroupSearchFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))"
|
defaultActiveDirectoryGroupSearchFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))"
|
||||||
|
|
||||||
|
sAMAccountNameAttribute = "sAMAccountName"
|
||||||
|
// pwdLastSetAttribute is the date and time that the password for this account was last changed.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/adschema/a-pwdlastset
|
||||||
|
pwdLastSetAttribute = "pwdLastSet"
|
||||||
|
// userAccountControlAttribute represents a bitmap of user properties.
|
||||||
|
// https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
|
||||||
|
userAccountControlAttribute = "userAccountControl"
|
||||||
|
// userAccountControlComputedAttribute represents a bitmap of user properties.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/adschema/a-msds-user-account-control-computed
|
||||||
|
userAccountControlComputedAttribute = "msDS-User-Account-Control-Computed"
|
||||||
|
// 0x0002 ACCOUNTDISABLE in userAccountControl bitmap.
|
||||||
|
accountDisabledBitmapValue = 2
|
||||||
|
// 0x0010 UF_LOCKOUT in msDS-User-Account-Control-Computed bitmap.
|
||||||
|
accountLockedBitmapValue = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
type activeDirectoryUpstreamGenericLDAPImpl struct {
|
type activeDirectoryUpstreamGenericLDAPImpl struct {
|
||||||
@ -315,12 +334,21 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context,
|
|||||||
Filter: adUpstreamImpl.Spec().GroupSearch().Filter(),
|
Filter: adUpstreamImpl.Spec().GroupSearch().Filter(),
|
||||||
GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(),
|
GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(),
|
||||||
},
|
},
|
||||||
Dialer: c.ldapDialer,
|
Dialer: c.ldapDialer,
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){
|
||||||
|
"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID"),
|
||||||
|
},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
pwdLastSetAttribute: upstreamldap.AttributeUnchangedSinceLogin(pwdLastSetAttribute),
|
||||||
|
userAccountControlAttribute: validUserAccountControl,
|
||||||
|
userAccountControlComputedAttribute: validComputedUserAccountControl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if spec.GroupSearch.Attributes.GroupName == "" {
|
if spec.GroupSearch.Attributes.GroupName == "" {
|
||||||
config.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){defaultActiveDirectoryGroupNameAttributeName: upstreamldap.GroupSAMAccountNameWithDomainSuffix}
|
config.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
||||||
|
defaultActiveDirectoryGroupNameAttributeName: groupSAMAccountNameWithDomainSuffix,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conditions := upstreamwatchers.ValidateGenericLDAP(ctx, adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config)
|
conditions := upstreamwatchers.ValidateGenericLDAP(ctx, adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config)
|
||||||
@ -353,3 +381,84 @@ func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, ups
|
|||||||
log.Error(err, "failed to update status")
|
log.Error(err, "failed to update status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func microsoftUUIDFromBinaryAttr(attributeName string) func(entry *ldap.Entry) (string, error) {
|
||||||
|
// validation has already been done so we can just get the attribute...
|
||||||
|
return func(entry *ldap.Entry) (string, error) {
|
||||||
|
binaryUUID := entry.GetRawAttributeValue(attributeName)
|
||||||
|
return microsoftUUIDFromBinary(binaryUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func microsoftUUIDFromBinary(binaryUUID []byte) (string, error) {
|
||||||
|
uuidVal, err := uuid.FromBytes(binaryUUID) // start out with the RFC4122 version
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// then swap it because AD stores the first 3 fields little-endian rather than the expected
|
||||||
|
// big-endian.
|
||||||
|
uuidVal[0], uuidVal[1], uuidVal[2], uuidVal[3] = uuidVal[3], uuidVal[2], uuidVal[1], uuidVal[0]
|
||||||
|
uuidVal[4], uuidVal[5] = uuidVal[5], uuidVal[4]
|
||||||
|
uuidVal[6], uuidVal[7] = uuidVal[7], uuidVal[6]
|
||||||
|
return uuidVal.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) {
|
||||||
|
sAMAccountNameAttributeValues := entry.GetAttributeValues(sAMAccountNameAttribute)
|
||||||
|
|
||||||
|
if len(sAMAccountNameAttributeValues) != 1 {
|
||||||
|
return "", fmt.Errorf(`found %d values for attribute %q, but expected 1 result`,
|
||||||
|
len(sAMAccountNameAttributeValues), sAMAccountNameAttribute,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sAMAccountName := sAMAccountNameAttributeValues[0]
|
||||||
|
if len(sAMAccountName) == 0 {
|
||||||
|
return "", fmt.Errorf(`found empty value for attribute %q, but expected value to be non-empty`,
|
||||||
|
sAMAccountNameAttribute,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
distinguishedName := entry.DN
|
||||||
|
domain, err := getDomainFromDistinguishedName(distinguishedName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return sAMAccountName + "@" + domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainComponentsRegexp = regexp.MustCompile(",DC=|,dc=")
|
||||||
|
|
||||||
|
func getDomainFromDistinguishedName(distinguishedName string) (string, error) {
|
||||||
|
domainComponents := domainComponentsRegexp.Split(distinguishedName, -1)
|
||||||
|
if len(domainComponents) == 1 {
|
||||||
|
return "", fmt.Errorf("did not find domain components in group dn: %s", distinguishedName)
|
||||||
|
}
|
||||||
|
return strings.Join(domainComponents[1:], "."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error {
|
||||||
|
userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlAttribute))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivated := userAccountControl & accountDisabledBitmapValue // bitwise and.
|
||||||
|
if deactivated != 0 {
|
||||||
|
return fmt.Errorf("user has been deactivated")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validComputedUserAccountControl(entry *ldap.Entry, _ provider.StoredRefreshAttributes) error {
|
||||||
|
userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlComputedAttribute))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
locked := userAccountControl & accountLockedBitmapValue // bitwise and
|
||||||
|
if locked != 0 {
|
||||||
|
return fmt.Errorf("user has been locked")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -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 activedirectoryupstreamwatcher
|
package activedirectoryupstreamwatcher
|
||||||
@ -220,7 +220,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a copy with targeted changes.
|
// Make a copy with targeted changes.
|
||||||
@ -536,7 +541,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -592,7 +602,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: "sAMAccountName",
|
GroupNameAttribute: "sAMAccountName",
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -651,7 +666,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -710,7 +730,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
@ -768,7 +793,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -897,7 +927,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1021,8 +1056,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
},
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, UID: testResourceUID, Generation: 1234},
|
||||||
@ -1072,7 +1111,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1271,8 +1315,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))",
|
Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))",
|
||||||
GroupNameAttribute: "sAMAccountName",
|
GroupNameAttribute: "sAMAccountName",
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": upstreamldap.GroupSAMAccountNameWithDomainSuffix},
|
GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": groupSAMAccountNameWithDomainSuffix},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1324,7 +1373,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1380,7 +1434,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1430,7 +1489,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1626,7 +1690,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Filter: testGroupSearchFilter,
|
Filter: testGroupSearchFilter,
|
||||||
GroupNameAttribute: testGroupNameAttrName,
|
GroupNameAttribute: testGroupNameAttrName,
|
||||||
},
|
},
|
||||||
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")},
|
UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
"pwdLastSet": upstreamldap.AttributeUnchangedSinceLogin("pwdLastSet"),
|
||||||
|
"userAccountControl": validUserAccountControl,
|
||||||
|
"msDS-User-Account-Control-Computed": validComputedUserAccountControl,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{
|
||||||
@ -1753,6 +1822,16 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[k]).Pointer())
|
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[k]).Pointer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks
|
||||||
|
actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks
|
||||||
|
copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
|
||||||
|
actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{}
|
||||||
|
require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks))
|
||||||
|
for k, v := range expectedRefreshAttributeChecks {
|
||||||
|
require.NotNil(t, actualRefreshAttributeChecks[k])
|
||||||
|
require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualRefreshAttributeChecks[k]).Pointer())
|
||||||
|
}
|
||||||
|
|
||||||
require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig)
|
require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1800,3 +1879,270 @@ func normalizeActiveDirectoryUpstreams(upstreams []v1alpha1.ActiveDirectoryIdent
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGroupSAMAccountNameWithDomainSuffix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entry *ldap.Entry
|
||||||
|
wantResult string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path with DN and valid sAMAccountName",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "CN=animals,OU=Users,OU=pinniped-ad,DC=mycompany,DC=example,DC=com",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantResult: "Mammals@mycompany.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no domain components in DN",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "no-domain-components",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "did not find domain components in group dn: no-domain-components",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple values for sAMAccountName attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "CN=animals,OU=Users,OU=pinniped-ad,DC=mycompany,DC=example,DC=com",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals", "Eukaryotes"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "found 2 values for attribute \"sAMAccountName\", but expected 1 result",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no values for sAMAccountName attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "CN=animals,OU=Users,OU=pinniped-ad,DC=mycompany,DC=example,DC=com",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
ldap.NewEntryAttribute("sAMAccountName", []string{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "found 0 values for attribute \"sAMAccountName\", but expected 1 result",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
suffixedSAMAccountName, err := groupSAMAccountNameWithDomainSuffix(tt.entry)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.wantResult, suffixedSAMAccountName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMicrosoftFormattedUUID(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
binaryUUID []byte
|
||||||
|
wantString string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
binaryUUID: []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"),
|
||||||
|
wantString: "04030201-0605-0807-0910-111213141516",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not the right length",
|
||||||
|
binaryUUID: []byte("2\xf8\xb0\xaa\xb6V\xb1D\x8b(\xee"),
|
||||||
|
wantErr: "invalid UUID (got 11 bytes)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actualUUIDString, err := microsoftUUIDFromBinary(tt.binaryUUID)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.wantString, actualUUIDString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDomainFromDistinguishedName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
distinguishedName string
|
||||||
|
wantDomain string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
distinguishedName: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
||||||
|
wantDomain: "activedirectory.mycompany.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lowercased happy path",
|
||||||
|
distinguishedName: "cn=Mammals,ou=Users,ou=pinniped-ad,dc=activedirectory,dc=mycompany,dc=example,dc=com",
|
||||||
|
wantDomain: "activedirectory.mycompany.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no domain components",
|
||||||
|
distinguishedName: "not-a-dn",
|
||||||
|
wantErr: "did not find domain components in group dn: not-a-dn",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actualDomain, err := getDomainFromDistinguishedName(tt.distinguishedName)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tt.wantDomain, actualDomain)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidUserAccountControl(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entry *ldap.Entry
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy normal user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"512"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "happy user whose password doesn't expire",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"65536"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deactivated user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"514"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "user has been deactivated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-integer result",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "userAccountControl",
|
||||||
|
Values: []string{"not-an-int"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "strconv.Atoi: parsing \"not-an-int\": invalid syntax",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validUserAccountControl(tt.entry, provider.StoredRefreshAttributes{})
|
||||||
|
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidComputedUserAccountControl(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entry *ldap.Entry
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy normal user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "msDS-User-Account-Control-Computed",
|
||||||
|
Values: []string{"0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "locked user",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "msDS-User-Account-Control-Computed",
|
||||||
|
Values: []string{"16"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "user has been locked",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-integer result",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "msDS-User-Account-Control-Computed",
|
||||||
|
Values: []string{"not-an-int"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "strconv.Atoi: parsing \"not-an-int\": invalid syntax",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
tt := test
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validComputedUserAccountControl(tt.entry, provider.StoredRefreshAttributes{})
|
||||||
|
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 supervisorconfig
|
package supervisorconfig
|
||||||
@ -11,10 +11,10 @@ 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/clock"
|
|
||||||
"k8s.io/apimachinery/pkg/util/errors"
|
"k8s.io/apimachinery/pkg/util/errors"
|
||||||
"k8s.io/client-go/util/retry"
|
"k8s.io/client-go/util/retry"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||||
|
@ -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 supervisorconfig
|
package supervisorconfig
|
||||||
@ -20,8 +20,8 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"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/util/clock"
|
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
||||||
@ -116,7 +116,7 @@ func TestSync(t *testing.T) {
|
|||||||
// Set this at the last second to allow for injection of server override.
|
// Set this at the last second to allow for injection of server override.
|
||||||
subject = NewFederationDomainWatcherController(
|
subject = NewFederationDomainWatcherController(
|
||||||
providersSetter,
|
providersSetter,
|
||||||
clock.NewFakeClock(frozenNow),
|
clocktesting.NewFakeClock(frozenNow),
|
||||||
pinnipedAPIClient,
|
pinnipedAPIClient,
|
||||||
federationDomainInformers.Config().V1alpha1().FederationDomains(),
|
federationDomainInformers.Config().V1alpha1().FederationDomains(),
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
|
@ -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 oidcupstreamwatcher implements a controller which watches OIDCIdentityProviders.
|
// Package oidcupstreamwatcher implements a controller which watches OIDCIdentityProviders.
|
||||||
@ -324,6 +324,11 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, issuerURLCondition := validateHTTPSURL(upstream.Spec.Issuer, "issuer", reasonUnreachable)
|
||||||
|
if issuerURLCondition != nil {
|
||||||
|
return issuerURLCondition
|
||||||
|
}
|
||||||
|
|
||||||
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
|
const klogLevelTrace = 6
|
||||||
@ -359,46 +364,35 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if additionalDiscoveryClaims.RevocationEndpoint != "" {
|
if additionalDiscoveryClaims.RevocationEndpoint != "" {
|
||||||
// Found a revocation URL. Try to parse it.
|
// Found a revocation URL. Validate it.
|
||||||
revocationURL, err := url.Parse(additionalDiscoveryClaims.RevocationEndpoint)
|
revocationURL, revocationURLCondition := validateHTTPSURL(
|
||||||
if err != nil {
|
additionalDiscoveryClaims.RevocationEndpoint,
|
||||||
return &v1alpha1.Condition{
|
"revocation endpoint",
|
||||||
Type: typeOIDCDiscoverySucceeded,
|
reasonInvalidResponse,
|
||||||
Status: v1alpha1.ConditionFalse,
|
)
|
||||||
Reason: reasonInvalidResponse,
|
if revocationURLCondition != nil {
|
||||||
Message: fmt.Sprintf("failed to parse revocation endpoint URL: %v", err),
|
return revocationURLCondition
|
||||||
}
|
|
||||||
}
|
|
||||||
// Don't want to send refresh tokens to an insecure revocation endpoint, so require that it use https.
|
|
||||||
if revocationURL.Scheme != "https" {
|
|
||||||
return &v1alpha1.Condition{
|
|
||||||
Type: typeOIDCDiscoverySucceeded,
|
|
||||||
Status: v1alpha1.ConditionFalse,
|
|
||||||
Reason: reasonInvalidResponse,
|
|
||||||
Message: fmt.Sprintf(`revocation endpoint URL scheme must be "https", not %q`, revocationURL.Scheme),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Remember the URL for later use.
|
// Remember the URL for later use.
|
||||||
result.RevocationURL = revocationURL
|
result.RevocationURL = revocationURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse out and validate the discovered authorize endpoint.
|
_, authorizeURLCondition := validateHTTPSURL(
|
||||||
authURL, err := url.Parse(discoveredProvider.Endpoint().AuthURL)
|
discoveredProvider.Endpoint().AuthURL,
|
||||||
if err != nil {
|
"authorization endpoint",
|
||||||
return &v1alpha1.Condition{
|
reasonInvalidResponse,
|
||||||
Type: typeOIDCDiscoverySucceeded,
|
)
|
||||||
Status: v1alpha1.ConditionFalse,
|
if authorizeURLCondition != nil {
|
||||||
Reason: reasonInvalidResponse,
|
return authorizeURLCondition
|
||||||
Message: fmt.Sprintf("failed to parse authorization endpoint URL: %v", err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if authURL.Scheme != "https" {
|
|
||||||
return &v1alpha1.Condition{
|
_, tokenURLCondition := validateHTTPSURL(
|
||||||
Type: typeOIDCDiscoverySucceeded,
|
discoveredProvider.Endpoint().TokenURL,
|
||||||
Status: v1alpha1.ConditionFalse,
|
"token endpoint",
|
||||||
Reason: reasonInvalidResponse,
|
reasonInvalidResponse,
|
||||||
Message: fmt.Sprintf(`authorization endpoint URL scheme must be "https", not %q`, authURL.Scheme),
|
)
|
||||||
}
|
if tokenURLCondition != nil {
|
||||||
|
return tokenURLCondition
|
||||||
}
|
}
|
||||||
|
|
||||||
// If everything is valid, update the result and set the condition to true.
|
// If everything is valid, update the result and set the condition to true.
|
||||||
@ -489,3 +483,32 @@ func truncateMostLongErr(err error) string {
|
|||||||
|
|
||||||
return msg[:max] + fmt.Sprintf(" [truncated %d chars]", len(msg)-max)
|
return msg[:max] + fmt.Sprintf(" [truncated %d chars]", len(msg)-max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateHTTPSURL(maybeHTTPSURL, endpointType, reason string) (*url.URL, *v1alpha1.Condition) {
|
||||||
|
parsedURL, err := url.Parse(maybeHTTPSURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &v1alpha1.Condition{
|
||||||
|
Type: typeOIDCDiscoverySucceeded,
|
||||||
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
Reason: reason,
|
||||||
|
Message: fmt.Sprintf("failed to parse %s URL: %v", endpointType, truncateMostLongErr(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parsedURL.Scheme != "https" {
|
||||||
|
return nil, &v1alpha1.Condition{
|
||||||
|
Type: typeOIDCDiscoverySucceeded,
|
||||||
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
Reason: reason,
|
||||||
|
Message: fmt.Sprintf(`%s URL '%s' must have "https" scheme, not %q`, endpointType, maybeHTTPSURL, parsedURL.Scheme),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parsedURL.Query()) != 0 || parsedURL.Fragment != "" {
|
||||||
|
return nil, &v1alpha1.Condition{
|
||||||
|
Type: typeOIDCDiscoverySucceeded,
|
||||||
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
Reason: reason,
|
||||||
|
Message: fmt.Sprintf(`%s URL '%s' cannot contain query or fragment component`, endpointType, maybeHTTPSURL),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedURL, nil
|
||||||
|
}
|
||||||
|
@ -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 oidcupstreamwatcher
|
package oidcupstreamwatcher
|
||||||
@ -91,7 +91,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
||||||
secretInformer,
|
secretInformer,
|
||||||
testLog,
|
testLog.Logger,
|
||||||
withInformer.WithInformer,
|
withInformer.WithInformer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -399,7 +399,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||||
Issuer: "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
Issuer: "%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||||
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
@ -410,11 +410,10 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
wantLogs: []string{
|
wantLogs: []string{
|
||||||
`oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol scheme \"\"" "issuer"="invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`,
|
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse issuer URL: parse \"%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\": invalid URL escape \"%in\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": unsupported protocol [truncated 9 chars]" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse issuer URL: parse \"%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\": invalid URL escape \"%in\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||||
},
|
},
|
||||||
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
@ -435,8 +434,145 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) {
|
|||||||
Status: "False",
|
Status: "False",
|
||||||
LastTransitionTime: now,
|
LastTransitionTime: now,
|
||||||
Reason: "Unreachable",
|
Reason: "Unreachable",
|
||||||
Message: `failed to perform OIDC discovery against "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
|
Message: `failed to parse issuer URL: parse "%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": invalid URL escape "%in"`,
|
||||||
Get "invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration": unsupported protocol [truncated 9 chars]`,
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer is insecure http URL",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||||
|
Issuer: strings.Replace(testIssuerURL, "https", "http", 1),
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have \"https\" scheme, not \"http\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
happyAdditionalAuthorizeParametersValidCondition,
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Unreachable",
|
||||||
|
Message: `issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have "https" scheme, not "http"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer contains a query param",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||||
|
Issuer: testIssuerURL + "?sub=foo",
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="issuer URL '` + testIssuerURL + "?sub=foo" + `' cannot contain query or fragment component" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="issuer URL '` + testIssuerURL + "?sub=foo" + `' cannot contain query or fragment component" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
happyAdditionalAuthorizeParametersValidCondition,
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Unreachable",
|
||||||
|
Message: `issuer URL '` + testIssuerURL + "?sub=foo" + `' cannot contain query or fragment component`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer contains a fragment",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||||
|
Issuer: testIssuerURL + "#fragment",
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="issuer URL '` + testIssuerURL + "#fragment" + `' cannot contain query or fragment component" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="issuer URL '` + testIssuerURL + "#fragment" + `' cannot contain query or fragment component" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
happyAdditionalAuthorizeParametersValidCondition,
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Unreachable",
|
||||||
|
Message: `issuer URL '` + testIssuerURL + "#fragment" + `' cannot contain query or fragment component`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -603,9 +739,9 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
|
|||||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
wantLogs: []string{
|
wantLogs: []string{
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL 'http://example.com/authorize' must have \"https\" scheme, not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL 'http://example.com/authorize' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
||||||
},
|
},
|
||||||
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
@ -626,7 +762,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
|
|||||||
Status: "False",
|
Status: "False",
|
||||||
LastTransitionTime: now,
|
LastTransitionTime: now,
|
||||||
Reason: "InvalidResponse",
|
Reason: "InvalidResponse",
|
||||||
Message: `authorization endpoint URL scheme must be "https", not "http"`,
|
Message: `authorization endpoint URL 'http://example.com/authorize' must have "https" scheme, not "http"`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -650,9 +786,9 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
|
|||||||
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
wantLogs: []string{
|
wantLogs: []string{
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="revocation endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="revocation endpoint URL 'http://example.com/revoke' must have \"https\" scheme, not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="revocation endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="revocation endpoint URL 'http://example.com/revoke' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
||||||
},
|
},
|
||||||
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
@ -673,7 +809,148 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana
|
|||||||
Status: "False",
|
Status: "False",
|
||||||
LastTransitionTime: now,
|
LastTransitionTime: now,
|
||||||
Reason: "InvalidResponse",
|
Reason: "InvalidResponse",
|
||||||
Message: `revocation endpoint URL scheme must be "https", not "http"`,
|
Message: `revocation endpoint URL 'http://example.com/revoke' must have "https" scheme, not "http"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer returns insecure token URL",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||||
|
Issuer: testIssuerURL + "/insecure-token-url",
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="token endpoint URL 'http://example.com/token' must have \"https\" scheme, not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="token endpoint URL 'http://example.com/token' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
happyAdditionalAuthorizeParametersValidCondition,
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "InvalidResponse",
|
||||||
|
Message: `token endpoint URL 'http://example.com/token' must have "https" scheme, not "http"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer returns no token URL",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||||
|
Issuer: testIssuerURL + "/missing-token-url",
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="token endpoint URL '' must have \"https\" scheme, not \"\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="token endpoint URL '' must have \"https\" scheme, not \"\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
happyAdditionalAuthorizeParametersValidCondition,
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "InvalidResponse",
|
||||||
|
Message: `token endpoint URL '' must have "https" scheme, not ""`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer returns no auth URL",
|
||||||
|
inputUpstreams: []runtime.Object{&v1alpha1.OIDCIdentityProvider{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Spec: v1alpha1.OIDCIdentityProviderSpec{
|
||||||
|
Issuer: testIssuerURL + "/missing-auth-url",
|
||||||
|
TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testIssuerCABase64},
|
||||||
|
Client: v1alpha1.OIDCClient{SecretName: testSecretName},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
inputSecrets: []runtime.Object{&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testSecretName},
|
||||||
|
Type: "secrets.pinniped.dev/oidc-client",
|
||||||
|
Data: testValidSecretData,
|
||||||
|
}},
|
||||||
|
wantErr: controllerlib.ErrSyntheticRequeue.Error(),
|
||||||
|
wantLogs: []string{
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL '' must have \"https\" scheme, not \"\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
`oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`,
|
||||||
|
`oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL '' must have \"https\" scheme, not \"\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`,
|
||||||
|
},
|
||||||
|
wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{},
|
||||||
|
wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName},
|
||||||
|
Status: v1alpha1.OIDCIdentityProviderStatus{
|
||||||
|
Phase: "Error",
|
||||||
|
Conditions: []v1alpha1.Condition{
|
||||||
|
happyAdditionalAuthorizeParametersValidCondition,
|
||||||
|
{
|
||||||
|
Type: "ClientCredentialsValid",
|
||||||
|
Status: "True",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "Success",
|
||||||
|
Message: "loaded client credentials",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "OIDCDiscoverySucceeded",
|
||||||
|
Status: "False",
|
||||||
|
LastTransitionTime: now,
|
||||||
|
Reason: "InvalidResponse",
|
||||||
|
Message: `authorization endpoint URL '' must have "https" scheme, not ""`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1123,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.New(t)
|
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"},
|
||||||
@ -1134,7 +1411,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs
|
|||||||
fakePinnipedClient,
|
fakePinnipedClient,
|
||||||
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
|
||||||
kubeInformers.Core().V1().Secrets(),
|
kubeInformers.Core().V1().Secrets(),
|
||||||
testLog,
|
testLog.Logger,
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1253,6 +1530,7 @@ func newTestIssuer(t *testing.T) (string, string) {
|
|||||||
Issuer: testURL,
|
Issuer: testURL,
|
||||||
AuthURL: "https://example.com/authorize",
|
AuthURL: "https://example.com/authorize",
|
||||||
RevocationURL: "https://example.com/revoke",
|
RevocationURL: "https://example.com/revoke",
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1263,6 +1541,7 @@ func newTestIssuer(t *testing.T) (string, string) {
|
|||||||
Issuer: testURL + "/valid-without-revocation",
|
Issuer: testURL + "/valid-without-revocation",
|
||||||
AuthURL: "https://example.com/authorize",
|
AuthURL: "https://example.com/authorize",
|
||||||
RevocationURL: "", // none
|
RevocationURL: "", // none
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1270,8 +1549,9 @@ func newTestIssuer(t *testing.T) (string, string) {
|
|||||||
mux.HandleFunc("/invalid/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/invalid/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
Issuer: testURL + "/invalid",
|
Issuer: testURL + "/invalid",
|
||||||
AuthURL: "%",
|
AuthURL: "%",
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1282,6 +1562,7 @@ func newTestIssuer(t *testing.T) (string, string) {
|
|||||||
Issuer: testURL + "/invalid-revocation-url",
|
Issuer: testURL + "/invalid-revocation-url",
|
||||||
AuthURL: "https://example.com/authorize",
|
AuthURL: "https://example.com/authorize",
|
||||||
RevocationURL: "%",
|
RevocationURL: "%",
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1289,18 +1570,52 @@ func newTestIssuer(t *testing.T) (string, string) {
|
|||||||
mux.HandleFunc("/insecure/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/insecure/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
Issuer: testURL + "/insecure",
|
Issuer: testURL + "/insecure",
|
||||||
AuthURL: "http://example.com/authorize",
|
AuthURL: "http://example.com/authorize",
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// At "/insecure", serve an issuer that returns an insecure authorization URL (not https://).
|
// At "/insecure-revocation-url", serve an issuer that returns an insecure revocation URL (not https://).
|
||||||
mux.HandleFunc("/insecure-revocation-url/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/insecure-revocation-url/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("content-type", "application/json")
|
w.Header().Set("content-type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(&providerJSON{
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
Issuer: testURL + "/insecure-revocation-url",
|
Issuer: testURL + "/insecure-revocation-url",
|
||||||
AuthURL: "https://example.com/authorize",
|
AuthURL: "https://example.com/authorize",
|
||||||
RevocationURL: "http://example.com/revoke",
|
RevocationURL: "http://example.com/revoke",
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// At "/insecure-token-url", serve an issuer that returns an insecure token URL (not https://).
|
||||||
|
mux.HandleFunc("/insecure-token-url/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
|
Issuer: testURL + "/insecure-token-url",
|
||||||
|
AuthURL: "https://example.com/authorize",
|
||||||
|
RevocationURL: "https://example.com/revoke",
|
||||||
|
TokenURL: "http://example.com/token",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// At "/missing-token-url", serve an issuer that returns no token URL (is required by the spec unless it's an idp which only supports
|
||||||
|
// implicit flow, which we don't support). So for our purposes we need to always get a token url
|
||||||
|
mux.HandleFunc("/missing-token-url/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
|
Issuer: testURL + "/missing-token-url",
|
||||||
|
AuthURL: "https://example.com/authorize",
|
||||||
|
RevocationURL: "https://example.com/revoke",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// At "/missing-auth-url", serve an issuer that returns no auth URL, which is required by the spec.
|
||||||
|
mux.HandleFunc("/missing-auth-url/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(&providerJSON{
|
||||||
|
Issuer: testURL + "/missing-auth-url",
|
||||||
|
RevocationURL: "https://example.com/revoke",
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1316,6 +1631,7 @@ func newTestIssuer(t *testing.T) (string, string) {
|
|||||||
Issuer: testURL + "/ends-with-slash/",
|
Issuer: testURL + "/ends-with-slash/",
|
||||||
AuthURL: "https://example.com/authorize",
|
AuthURL: "https://example.com/authorize",
|
||||||
RevocationURL: "https://example.com/revoke",
|
RevocationURL: "https://example.com/revoke",
|
||||||
|
TokenURL: "https://example.com/token",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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 supervisorstorage
|
package supervisorstorage
|
||||||
@ -13,9 +13,10 @@ import (
|
|||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
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/clock"
|
|
||||||
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/utils/clock"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
"k8s.io/utils/strings/slices"
|
"k8s.io/utils/strings/slices"
|
||||||
|
|
||||||
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
pinnipedcontroller "go.pinniped.dev/internal/controller"
|
||||||
@ -88,7 +89,7 @@ func GarbageCollectorController(
|
|||||||
|
|
||||||
func (c *garbageCollectorController) Sync(ctx controllerlib.Context) error {
|
func (c *garbageCollectorController) Sync(ctx controllerlib.Context) error {
|
||||||
// make sure we have a consistent, static meaning for the current time during the sync loop
|
// make sure we have a consistent, static meaning for the current time during the sync loop
|
||||||
frozenClock := clock.NewFakeClock(c.clock.Now())
|
frozenClock := clocktesting.NewFakeClock(c.clock.Now())
|
||||||
|
|
||||||
// The Sync method is triggered upon any change to any Secret, which would make this
|
// The Sync method is triggered upon any change to any Secret, which would make this
|
||||||
// controller too chatty, so it rate limits itself to a more reasonable interval.
|
// controller too chatty, so it rate limits itself to a more reasonable interval.
|
||||||
|
@ -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 supervisorstorage
|
package supervisorstorage
|
||||||
@ -18,11 +18,11 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"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/util/clock"
|
|
||||||
kubeinformers "k8s.io/client-go/informers"
|
kubeinformers "k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||||
kubetesting "k8s.io/client-go/testing"
|
kubetesting "k8s.io/client-go/testing"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/controllerlib"
|
"go.pinniped.dev/internal/controllerlib"
|
||||||
"go.pinniped.dev/internal/fositestorage/accesstoken"
|
"go.pinniped.dev/internal/fositestorage/accesstoken"
|
||||||
@ -127,13 +127,11 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
subject controllerlib.Controller
|
subject controllerlib.Controller
|
||||||
kubeInformerClient *kubernetesfake.Clientset
|
kubeInformerClient *kubernetesfake.Clientset
|
||||||
kubeClient *kubernetesfake.Clientset
|
kubeClient *kubernetesfake.Clientset
|
||||||
deleteOptions *[]metav1.DeleteOptions
|
|
||||||
deleteOptionsRecorder kubernetes.Interface
|
|
||||||
kubeInformers kubeinformers.SharedInformerFactory
|
kubeInformers kubeinformers.SharedInformerFactory
|
||||||
cancelContext context.Context
|
cancelContext context.Context
|
||||||
cancelContextCancelFunc context.CancelFunc
|
cancelContextCancelFunc context.CancelFunc
|
||||||
syncContext *controllerlib.Context
|
syncContext *controllerlib.Context
|
||||||
fakeClock *clock.FakeClock
|
fakeClock *clocktesting.FakeClock
|
||||||
frozenNow time.Time
|
frozenNow time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -144,7 +142,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
subject = GarbageCollectorController(
|
subject = GarbageCollectorController(
|
||||||
idpCache,
|
idpCache,
|
||||||
fakeClock,
|
fakeClock,
|
||||||
deleteOptionsRecorder,
|
kubeClient,
|
||||||
kubeInformers.Core().V1().Secrets(),
|
kubeInformers.Core().V1().Secrets(),
|
||||||
controllerlib.WithInformer,
|
controllerlib.WithInformer,
|
||||||
)
|
)
|
||||||
@ -172,11 +170,9 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
kubeInformerClient = kubernetesfake.NewSimpleClientset()
|
||||||
kubeClient = kubernetesfake.NewSimpleClientset()
|
kubeClient = kubernetesfake.NewSimpleClientset()
|
||||||
deleteOptions = &[]metav1.DeleteOptions{}
|
|
||||||
deleteOptionsRecorder = testutil.NewDeleteOptionsRecorder(kubeClient, deleteOptions)
|
|
||||||
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
|
||||||
frozenNow = time.Now().UTC()
|
frozenNow = time.Now().UTC()
|
||||||
fakeClock = clock.NewFakeClock(frozenNow)
|
fakeClock = clocktesting.NewFakeClock(frozenNow)
|
||||||
|
|
||||||
unrelatedSecret := &corev1.Secret{
|
unrelatedSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@ -252,18 +248,11 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "first expired secret"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "first expired secret", testutil.NewPreconditions("uid-123", "rv-456")),
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "second expired secret"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "second expired secret", testutil.NewPreconditions("uid-789", "rv-555")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-456"),
|
|
||||||
testutil.NewPreconditions("uid-789", "rv-555"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
list, err := kubeClient.CoreV1().Secrets(installedInNamespace).List(context.Background(), metav1.ListOptions{})
|
list, err := kubeClient.CoreV1().Secrets(installedInNamespace).List(context.Background(), metav1.ListOptions{})
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.Len(list.Items, 2)
|
r.Len(list.Items, 2)
|
||||||
@ -514,18 +503,11 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// Both authcode session secrets are deleted.
|
// Both authcode session secrets are deleted.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "activeOIDCAuthcodeSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "activeOIDCAuthcodeSession", testutil.NewPreconditions("uid-123", "rv-123")),
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "inactiveOIDCAuthcodeSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "inactiveOIDCAuthcodeSession", testutil.NewPreconditions("uid-456", "rv-456")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-123"),
|
|
||||||
testutil.NewPreconditions("uid-456", "rv-456"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -590,16 +572,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// The invalid authcode session secrets is still deleted because it is expired.
|
// The invalid authcode session secrets is still deleted because it is expired.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "invalidOIDCAuthcodeSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "invalidOIDCAuthcodeSession", testutil.NewPreconditions("uid-123", "rv-123")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-123"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -666,16 +642,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// The authcode session secrets is still deleted because it is expired.
|
// The authcode session secrets is still deleted because it is expired.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "wrongProviderNameOIDCAuthcodeSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "wrongProviderNameOIDCAuthcodeSession", testutil.NewPreconditions("uid-123", "rv-123")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-123"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -742,16 +712,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// The authcode session secrets is still deleted because it is expired.
|
// The authcode session secrets is still deleted because it is expired.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "wrongProviderNameOIDCAuthcodeSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "wrongProviderNameOIDCAuthcodeSession", testutil.NewPreconditions("uid-123", "rv-123")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-123"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -936,16 +900,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// The authcode session secrets is deleted.
|
// The authcode session secrets is deleted.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "activeOIDCAuthcodeSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "activeOIDCAuthcodeSession", testutil.NewPreconditions("uid-123", "rv-123")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-123"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1063,18 +1021,11 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// Both session secrets are deleted.
|
// Both session secrets are deleted.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "offlineAccessGrantedOIDCAccessTokenSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "offlineAccessGrantedOIDCAccessTokenSession", testutil.NewPreconditions("uid-123", "rv-123")),
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "offlineAccessNotGrantedOIDCAccessTokenSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "offlineAccessNotGrantedOIDCAccessTokenSession", testutil.NewPreconditions("uid-456", "rv-456")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-123"),
|
|
||||||
testutil.NewPreconditions("uid-456", "rv-456"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1358,16 +1309,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// The secret is deleted.
|
// The secret is deleted.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "oidcRefreshSession"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "oidcRefreshSession", testutil.NewPreconditions("uid-123", "rv-123")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
r.ElementsMatch(
|
|
||||||
[]metav1.DeleteOptions{
|
|
||||||
testutil.NewPreconditions("uid-123", "rv-123"),
|
|
||||||
},
|
|
||||||
*deleteOptions,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1376,8 +1321,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// Add a secret that will expire in 20 seconds.
|
// Add a secret that will expire in 20 seconds.
|
||||||
expiredSecret := &corev1.Secret{
|
expiredSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "expired secret",
|
Name: "expired secret",
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
|
UID: "uid-747",
|
||||||
|
ResourceVersion: "rv-609",
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(20 * time.Second).Format(time.RFC3339),
|
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(20 * time.Second).Format(time.RFC3339),
|
||||||
},
|
},
|
||||||
@ -1415,7 +1362,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
// It should have deleted the expired secret.
|
// It should have deleted the expired secret.
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "expired secret"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "expired secret", testutil.NewPreconditions("uid-747", "rv-609")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
@ -1441,8 +1388,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
r.NoError(kubeClient.Tracker().Add(malformedSecret))
|
r.NoError(kubeClient.Tracker().Add(malformedSecret))
|
||||||
expiredSecret := &corev1.Secret{
|
expiredSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "expired secret",
|
Name: "expired secret",
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
|
UID: "uid-748",
|
||||||
|
ResourceVersion: "rv-608",
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(-time.Second).Format(time.RFC3339),
|
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(-time.Second).Format(time.RFC3339),
|
||||||
},
|
},
|
||||||
@ -1458,7 +1407,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "expired secret"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "expired secret", testutil.NewPreconditions("uid-748", "rv-608")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
@ -1473,8 +1422,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
erroringSecret := &corev1.Secret{
|
erroringSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "erroring secret",
|
Name: "erroring secret",
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
|
UID: "uid-111",
|
||||||
|
ResourceVersion: "rv-222",
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(-time.Second).Format(time.RFC3339),
|
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(-time.Second).Format(time.RFC3339),
|
||||||
},
|
},
|
||||||
@ -1490,8 +1441,10 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
})
|
})
|
||||||
expiredSecret := &corev1.Secret{
|
expiredSecret := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "expired secret",
|
Name: "expired secret",
|
||||||
Namespace: installedInNamespace,
|
Namespace: installedInNamespace,
|
||||||
|
UID: "uid-333",
|
||||||
|
ResourceVersion: "rv-444",
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(-time.Second).Format(time.RFC3339),
|
"storage.pinniped.dev/garbage-collect-after": frozenNow.Add(-time.Second).Format(time.RFC3339),
|
||||||
},
|
},
|
||||||
@ -1507,8 +1460,8 @@ func TestGarbageCollectorControllerSync(t *testing.T) {
|
|||||||
|
|
||||||
r.ElementsMatch(
|
r.ElementsMatch(
|
||||||
[]kubetesting.Action{
|
[]kubetesting.Action{
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "erroring secret"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "erroring secret", testutil.NewPreconditions("uid-111", "rv-222")),
|
||||||
kubetesting.NewDeleteAction(secretsGVR, installedInNamespace, "expired secret"),
|
kubetesting.NewDeleteActionWithOptions(secretsGVR, installedInNamespace, "expired secret", testutil.NewPreconditions("uid-333", "rv-444")),
|
||||||
},
|
},
|
||||||
kubeClient.Actions(),
|
kubeClient.Actions(),
|
||||||
)
|
)
|
||||||
|
@ -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 controllermanager provides an entrypoint into running all of the controllers that run as
|
// Package controllermanager provides an entrypoint into running all of the controllers that run as
|
||||||
@ -9,10 +9,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
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/klog/v2/klogr"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
|
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||||
pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
|
pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
|
||||||
|
@ -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 crud
|
package crud
|
||||||
@ -18,9 +18,9 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"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/util/clock"
|
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStorage(t *testing.T) {
|
func TestStorage(t *testing.T) {
|
||||||
@ -62,7 +62,7 @@ func TestStorage(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
resource string
|
resource string
|
||||||
mocks func(*testing.T, mocker)
|
mocks func(*testing.T, mocker)
|
||||||
run func(*testing.T, Storage, *clock.FakeClock) error
|
run func(*testing.T, Storage, *clocktesting.FakeClock) error
|
||||||
wantActions []coretesting.Action
|
wantActions []coretesting.Action
|
||||||
wantSecrets []corev1.Secret
|
wantSecrets []corev1.Secret
|
||||||
wantErr string
|
wantErr string
|
||||||
@ -71,7 +71,7 @@ func TestStorage(t *testing.T) {
|
|||||||
name: "get non-existent",
|
name: "get non-existent",
|
||||||
resource: "authcode",
|
resource: "authcode",
|
||||||
mocks: nil,
|
mocks: nil,
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
_, err := storage.Get(ctx, "not-exists", nil)
|
_, err := storage.Get(ctx, "not-exists", nil)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
@ -85,7 +85,7 @@ func TestStorage(t *testing.T) {
|
|||||||
name: "delete non-existent",
|
name: "delete non-existent",
|
||||||
resource: "tokens",
|
resource: "tokens",
|
||||||
mocks: nil,
|
mocks: nil,
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
return storage.Delete(ctx, "not-a-token")
|
return storage.Delete(ctx, "not-a-token")
|
||||||
},
|
},
|
||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
@ -98,7 +98,7 @@ func TestStorage(t *testing.T) {
|
|||||||
name: "delete non-existent by label",
|
name: "delete non-existent by label",
|
||||||
resource: "tokens",
|
resource: "tokens",
|
||||||
mocks: nil,
|
mocks: nil,
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
||||||
},
|
},
|
||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
@ -113,7 +113,7 @@ func TestStorage(t *testing.T) {
|
|||||||
name: "create and get",
|
name: "create and get",
|
||||||
resource: "access-tokens",
|
resource: "access-tokens",
|
||||||
mocks: nil,
|
mocks: nil,
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode1)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode1)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -177,7 +177,7 @@ func TestStorage(t *testing.T) {
|
|||||||
name: "create multiple, each gets the correct lifetime timestamp",
|
name: "create multiple, each gets the correct lifetime timestamp",
|
||||||
resource: "access-tokens",
|
resource: "access-tokens",
|
||||||
mocks: nil,
|
mocks: nil,
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
data := &testJSON{Data: "create1"}
|
data := &testJSON{Data: "create1"}
|
||||||
rv1, err := storage.Create(ctx, "sig1", data, nil)
|
rv1, err := storage.Create(ctx, "sig1", data, nil)
|
||||||
require.Empty(t, rv1) // fake client does not set this
|
require.Empty(t, rv1) // fake client does not set this
|
||||||
@ -272,7 +272,7 @@ func TestStorage(t *testing.T) {
|
|||||||
name: "create and get with additional labels",
|
name: "create and get with additional labels",
|
||||||
resource: "access-tokens",
|
resource: "access-tokens",
|
||||||
mocks: nil,
|
mocks: nil,
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode1)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode1)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -360,7 +360,7 @@ func TestStorage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode2)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode2)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -429,7 +429,7 @@ func TestStorage(t *testing.T) {
|
|||||||
return false, nil, nil // we mutated the secret in place but we do not "handle" it
|
return false, nil, nil // we mutated the secret in place but we do not "handle" it
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -521,7 +521,7 @@ func TestStorage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode2)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode2)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -615,7 +615,7 @@ func TestStorage(t *testing.T) {
|
|||||||
Type: "storage.pinniped.dev/walruses",
|
Type: "storage.pinniped.dev/walruses",
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
||||||
},
|
},
|
||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
@ -696,7 +696,7 @@ func TestStorage(t *testing.T) {
|
|||||||
return true, nil, fmt.Errorf("some delete error")
|
return true, nil, fmt.Errorf("some delete error")
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
||||||
},
|
},
|
||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
@ -749,7 +749,7 @@ func TestStorage(t *testing.T) {
|
|||||||
return true, nil, fmt.Errorf("some listing error")
|
return true, nil, fmt.Errorf("some listing error")
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
return storage.DeleteByLabel(ctx, "additionalLabel", "matching-value")
|
||||||
},
|
},
|
||||||
wantActions: []coretesting.Action{
|
wantActions: []coretesting.Action{
|
||||||
@ -783,7 +783,7 @@ func TestStorage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -847,7 +847,7 @@ func TestStorage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -911,7 +911,7 @@ func TestStorage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -975,7 +975,7 @@ func TestStorage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, storage Storage, fakeClock *clock.FakeClock) error {
|
run: func(t *testing.T, storage Storage, fakeClock *clocktesting.FakeClock) error {
|
||||||
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
|
||||||
require.NotEmpty(t, signature)
|
require.NotEmpty(t, signature)
|
||||||
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
|
||||||
@ -1025,7 +1025,7 @@ func TestStorage(t *testing.T) {
|
|||||||
tt.mocks(t, client)
|
tt.mocks(t, client)
|
||||||
}
|
}
|
||||||
secrets := client.CoreV1().Secrets(namespace)
|
secrets := client.CoreV1().Secrets(namespace)
|
||||||
fakeClock := clock.NewFakeClock(fakeNow)
|
fakeClock := clocktesting.NewFakeClock(fakeNow)
|
||||||
storage := New(tt.resource, secrets, fakeClock.Now, lifetime)
|
storage := New(tt.resource, secrets, fakeClock.Now, lifetime)
|
||||||
|
|
||||||
err := tt.run(t, storage, fakeClock)
|
err := tt.run(t, storage, fakeClock)
|
||||||
|
@ -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 accesstoken
|
package accesstoken
|
||||||
@ -16,10 +16,10 @@ import (
|
|||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
@ -53,7 +53,7 @@ func TestAccessTokenStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
@ -122,7 +122,7 @@ func TestAccessTokenStorageRevocation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/access-token",
|
Type: "storage.pinniped.dev/access-token",
|
||||||
@ -276,7 +276,7 @@ func TestCreateWithoutRequesterID(t *testing.T) {
|
|||||||
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) {
|
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) {
|
||||||
client := fake.NewSimpleClientset()
|
client := fake.NewSimpleClientset()
|
||||||
secrets := client.CoreV1().Secrets(namespace)
|
secrets := client.CoreV1().Secrets(namespace)
|
||||||
return context.Background(), client, secrets, New(secrets, clock.NewFakeClock(fakeNow).Now, lifetime)
|
return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadFromSecret(t *testing.T) {
|
func TestReadFromSecret(t *testing.T) {
|
||||||
|
@ -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 authorizationcode
|
package authorizationcode
|
||||||
@ -196,32 +196,38 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
|
|||||||
"client": {
|
"client": {
|
||||||
"id": ":NJ¸Ɣ8(黋馛ÄRɴJa¶z",
|
"id": ":NJ¸Ɣ8(黋馛ÄRɴJa¶z",
|
||||||
"client_secret": "UQ==",
|
"client_secret": "UQ==",
|
||||||
|
"rotated_secrets": [
|
||||||
|
"Bno=",
|
||||||
|
"0j8=",
|
||||||
|
"1c4="
|
||||||
|
],
|
||||||
"redirect_uris": [
|
"redirect_uris": [
|
||||||
"ǖ枭kʍ切厦ȳ箦;¥ʊXĝ奨誷傥祩d",
|
"ʊXĝ",
|
||||||
|
"Ƿ"
|
||||||
|
],
|
||||||
|
"grant_types": [
|
||||||
|
"祩d",
|
||||||
"zŇZ",
|
"zŇZ",
|
||||||
"優蒼ĊɌț訫DŽǽeʀO2ƚ\u0026N"
|
"優蒼ĊɌț訫DŽǽeʀO2ƚ\u0026N"
|
||||||
],
|
],
|
||||||
"grant_types": [
|
"response_types": [
|
||||||
"唐W6ɻ橩斚薛ɑƐ"
|
"唐W6ɻ橩斚薛ɑƐ"
|
||||||
],
|
],
|
||||||
"response_types": [
|
"scopes": [
|
||||||
"w",
|
"w",
|
||||||
"ǔŭe[u@阽羂ŷ-Ĵ½輢OÅ濲喾H"
|
"ǔŭe[u@阽羂ŷ-Ĵ½輢OÅ濲喾H"
|
||||||
],
|
],
|
||||||
"scopes": [
|
"audience": [
|
||||||
"G螩歐湡ƙı唡ɸğƎ\u0026胢輢Ƈĵƚ"
|
"G螩歐湡ƙı唡ɸğƎ\u0026胢輢Ƈĵƚ"
|
||||||
],
|
],
|
||||||
"audience": [
|
|
||||||
"ě"
|
|
||||||
],
|
|
||||||
"public": false,
|
"public": false,
|
||||||
"jwks_uri": "o*泞羅ʘ Ⱦķ瀊垰7ã\")",
|
"jwks_uri": "潌țjA9;焋Ēƕ",
|
||||||
"jwks": {
|
"jwks": {
|
||||||
"keys": [
|
"keys": [
|
||||||
{
|
{
|
||||||
"kty": "OKP",
|
"kty": "OKP",
|
||||||
"crv": "Ed25519",
|
"crv": "Ed25519",
|
||||||
"x": "nK9xgX_iN7u3u_i8YOO7ZRT_WK028Vd_nhtsUu7Eo6E",
|
"x": "LHMZ29A64WecPQSLotS8hfZ2mae0SR17CtPdnMDP7ZI",
|
||||||
"x5u": {
|
"x5u": {
|
||||||
"Scheme": "",
|
"Scheme": "",
|
||||||
"Opaque": "",
|
"Opaque": "",
|
||||||
@ -238,7 +244,24 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
|
|||||||
{
|
{
|
||||||
"kty": "OKP",
|
"kty": "OKP",
|
||||||
"crv": "Ed25519",
|
"crv": "Ed25519",
|
||||||
"x": "UbbswQgzWhfGCRlwQmMp6fw_HoIoqkIaKT-2XN2fuYU",
|
"x": "1PwKrC4qDe8cabzGTdA0NjuMJhAZAw7Bu7Tj9z2Y4pE",
|
||||||
|
"x5u": {
|
||||||
|
"Scheme": "",
|
||||||
|
"Opaque": "",
|
||||||
|
"User": null,
|
||||||
|
"Host": "",
|
||||||
|
"Path": "",
|
||||||
|
"RawPath": "",
|
||||||
|
"ForceQuery": false,
|
||||||
|
"RawQuery": "",
|
||||||
|
"Fragment": "",
|
||||||
|
"RawFragment": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "Ed25519",
|
||||||
|
"x": "j4b-Vld5buh_2KIpjjaDRJ8OY7l7d6XAumvDtVTT9BI",
|
||||||
"x5u": {
|
"x5u": {
|
||||||
"Scheme": "",
|
"Scheme": "",
|
||||||
"Opaque": "",
|
"Opaque": "",
|
||||||
@ -254,117 +277,128 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"token_endpoint_auth_method": "ƿʥǟȒ伉\u003cx¹T鼓c吏",
|
"token_endpoint_auth_method": "趀Ȁ;hYGe天蹗ĽǙ澅j翕q骽",
|
||||||
"request_uris": [
|
"request_uris": [
|
||||||
"Ć捘j]=谅ʑɑɮ$Ól4Ȟ",
|
"Ǐ蛓ȿ,JwwƐ\u003c涵ØƉKĵ",
|
||||||
",Q7钎漡臧n"
|
"Ȟú",
|
||||||
|
"Q7钎漡臧n栀,i"
|
||||||
],
|
],
|
||||||
"request_object_signing_alg": "3@¡廜+v,淬Ʋ4Dʧ呩锏緍场",
|
"request_object_signing_alg": "廜+v,淬Ʋ4Dʧ呩锏緍场脋",
|
||||||
"token_endpoint_auth_signing_alg": "(ưƓǴ罷ǹ~]ea胠"
|
"token_endpoint_auth_signing_alg": "ưƓǴ罷ǹ~]ea胠Ĺĩv絹b垇I"
|
||||||
},
|
},
|
||||||
"scopes": [
|
"scopes": [
|
||||||
"ĩv絹b垇IŕĩǀŻQ'k頂箨J-a稆",
|
"ĩǀŻQ'k頂箨J-a",
|
||||||
"啶#昏Q遐*\\髎bŸ"
|
"ɓ啶#昏Q遐*\\髎bŸ1慂U"
|
||||||
],
|
],
|
||||||
"grantedScopes": [
|
"grantedScopes": [
|
||||||
"慂UFƼĮǡ鑻Z"
|
"ƼĮǡ鑻Z¥篚h°ʣ£ǖ%\"砬ʍ"
|
||||||
],
|
],
|
||||||
"form": {
|
"form": {
|
||||||
"褾攚ŝlĆ厦駳骪l拁乖¡J¿Ƈ妔": [
|
"¡": [
|
||||||
"懧¥ɂĵ~Čyʊ恀c\"NJřðȿ/",
|
"Ła卦牟懧¥ɂĵ",
|
||||||
"裢?霃谥vƘ:ƿ/濔Aʉ\u003c",
|
"ɎǛƍdÚ慂+槰蚪i齥篗裢?霃谥vƘ:",
|
||||||
"ȭ$奍囀Dž悷鵱民撲ʓeŘ嬀j¤"
|
"/濔Aʉ\u003cS獾蔀OƭUǦ"
|
||||||
],
|
],
|
||||||
"诞": [
|
"民撲ʓeŘ嬀j¤囡莒汗狲N\u003cCq": [
|
||||||
"狲N\u003cCq罉ZPſĝEK郊©l",
|
"5ȏ樛ȧ.mĔ櫓Ǩ療騃Ǐ}ɟ",
|
||||||
"餚LJ/ɷȑ潠[ĝU噤'pX ",
|
"潠[ĝU噤'",
|
||||||
"Y妶ǵ!ȁu狍ɶȳsčɦƦ诱"
|
"ŁȗɉY妶ǵ!ȁ"
|
||||||
|
],
|
||||||
|
"褰ʎɰ癟VĎĢ婄磫绒u妔隤ʑƍš駎竪": [
|
||||||
|
"鱙翑ȲŻ麤ã桒嘞\\摗Ǘū稖咾鎅ǸÖ"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"fosite": {
|
"fosite": {
|
||||||
"Claims": {
|
"Claims": {
|
||||||
"JTI": "u妔隤ʑƍš駎竪0ɔ闏À1",
|
"JTI": "褗6巽ēđų蓼tùZ蛆鬣a\"ÙǞ0觢",
|
||||||
"Issuer": "麤ã桒嘞\\摗Ǘū稖咾鎅ǸÖ绝TF",
|
"Issuer": "j¦鲶H股ƲLŋZ-{",
|
||||||
"Subject": "巽ēđų蓼tùZ蛆鬣a\"ÙǞ0觢Û±",
|
"Subject": "ehpƧ蓟",
|
||||||
"Audience": [
|
"Audience": [
|
||||||
"H股ƲL",
|
"驜Ŗ~ů崧軒q腟u尿宲!"
|
||||||
"肟v\u0026đehpƧ",
|
|
||||||
"5^驜Ŗ~ů崧軒q腟u尿"
|
|
||||||
],
|
],
|
||||||
"Nonce": "ğ",
|
"Nonce": "ǎ^嫯R忑隯ƗƋ*L\u0026",
|
||||||
"ExpiresAt": "2016-11-22T21:33:58.460521133Z",
|
"ExpiresAt": "1989-06-02T14:40:29.613836765Z",
|
||||||
"IssuedAt": "1990-07-25T23:42:07.055978334Z",
|
"IssuedAt": "2052-03-26T02:39:27.882495556Z",
|
||||||
"RequestedAt": "1971-01-30T00:23:36.377684025Z",
|
"RequestedAt": "2038-04-06T10:46:24.698586972Z",
|
||||||
"AuthTime": "2088-11-09T12:09:14.051840239Z",
|
"AuthTime": "2003-01-05T11:30:18.206004879Z",
|
||||||
"AccessTokenHash": "蕖¤'+ʣȍ瓁U4鞀",
|
"AccessTokenHash": "ğǫ\\aȊ4ț髄Al",
|
||||||
"AuthenticationContextClassReference": "ʏÑęN\u003c_z",
|
"AuthenticationContextClassReference": "曓蓳n匟鯘磹*金爃鶴滱ůĮǐ_c3#",
|
||||||
"AuthenticationMethodsReference": "ț髄A",
|
"AuthenticationMethodsReferences": [
|
||||||
"CodeHash": "4磔_袻vÓG-壧丵礴鋈k蟵pAɂʅ",
|
"装ƹýĸŴB岺Ð嫹Sx镯荫őł疂ư墫"
|
||||||
|
],
|
||||||
|
"CodeHash": "\u0026鶡",
|
||||||
"Extra": {
|
"Extra": {
|
||||||
"#\u0026PƢ曰l騌蘙螤\\阏Đ镴Ƥm蔻ǭ\\鿞": 1677215584,
|
"rǓ\\BRë_g\"ʎ啴SƇMǃļū": {
|
||||||
"Y\u0026鶡萷ɵ啜s攦Ɩïdnǔ": {
|
"4撎胬龯,t猟i\u0026\u0026Q@ǤǟǗ": [
|
||||||
",t猟i\u0026\u0026Q@ǤǟǗǪ飘ȱF?Ƈ": {
|
1239190737
|
||||||
"~劰û橸ɽ銐ƭ?}H": null,
|
],
|
||||||
"癑勦e骲v0H晦XŘO溪V蔓": {
|
"飘ȱF?Ƈ畋": {
|
||||||
"碼Ǫ": false
|
"劰û橸ɽ銐ƭ?}HƟ玈鳚": null,
|
||||||
|
"骲v0H晦XŘO溪V蔓Ȍ+~ē埅Ȝ": {
|
||||||
|
"4Ǟ": false
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"钻煐ɨəÅDČ{Ȩʦ4撎": [
|
},
|
||||||
3684968178
|
"鑳绪": 2738428764
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Headers": {
|
"Headers": {
|
||||||
"Extra": {
|
"Extra": {
|
||||||
"ĊdŘ鸨EJ毕懴řĬń戹": {
|
"d謺錳4帳ŅǃĊ": 663773398,
|
||||||
"诳DT=3骜Ǹ,": {
|
"Ř鸨EJ": {
|
||||||
"\u003e": {
|
"Ǽǟ迍阊v\"豑觳翢砜": [
|
||||||
"ǰ": false
|
995342744
|
||||||
},
|
],
|
||||||
"ɁOƪ穋嶿鳈恱va": null
|
"ȏl鐉诳DT=3骜Ǹ": {
|
||||||
},
|
"厷ɁOƪ穋嶿鳈恱va|载ǰɱ汶C]ɲ": null,
|
||||||
"豑觳翢砜Fȏl": [
|
"荤Ý呐ʣ®DžȪǣǎǔ爣縗ɦü": {
|
||||||
927958776
|
"H :靥湤庤毩fɤȆʪ融ƆuŤn": true
|
||||||
]
|
}
|
||||||
},
|
}
|
||||||
"埅ȜʁɁ;Bd謺錳4帳Ņ": 388005986
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ExpiresAt": {
|
"ExpiresAt": {
|
||||||
"C]ɲ'=ĸ闒NȢȰ.醋": "1970-07-19T18:03:29.902062193Z",
|
"韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟ": "1970-04-27T04:31:30.902468229Z"
|
||||||
"fɤȆʪ融ƆuŤn": "2064-01-24T20:34:16.593152073Z",
|
|
||||||
"爣縗ɦüHêQ仏1ő": "2102-03-17T06:24:40.256846902Z"
|
|
||||||
},
|
},
|
||||||
"Username": "韁臯氃妪婝rȤ\"h丬鎒ơ娻}ɼƟ",
|
"Username": "髉龳ǽÙ",
|
||||||
"Subject": "闺髉龳ǽÙ龦O亾EW莛8嘶×"
|
"Subject": "\u0026¥潝邎Ȗ莅ŝǔ盕戙鵮碡ʯiŬŽ"
|
||||||
},
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
"providerUID": "鵮碡ʯiŬŽ非Ĝ眧Ĭ葜SŦ餧Ĭ倏4",
|
"providerUID": "Ĝ眧Ĭ",
|
||||||
"providerName": "nŐǛ3",
|
"providerName": "ʼn2ƋŢ觛ǂ焺nŐǛ",
|
||||||
"providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt",
|
"providerType": "ɥ闣ʬ橳(ý綃ʃʚƟ覣k眐4",
|
||||||
"oidc": {
|
"oidc": {
|
||||||
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬",
|
"upstreamRefreshToken": "tC嵽痊w",
|
||||||
"upstreamAccessToken": "巈環_ɑ"
|
"upstreamAccessToken": "a紽ǒ|鰽ŋ猊I",
|
||||||
|
"upstreamSubject": "妬\u003e6鉢緋uƴŤȱʀ",
|
||||||
|
"upstreamIssuer": ":設虝27就伒犘c"
|
||||||
},
|
},
|
||||||
"ldap": {
|
"ldap": {
|
||||||
"userDN": "ƍ蛊ʚ£:設虝27"
|
"userDN": "ɏȫ齁š%Op",
|
||||||
|
"extraRefreshAttributes": {
|
||||||
|
"T妼É4İ\u003e×1": "ʥ笿0D",
|
||||||
|
"÷驣7Ʀ澉1æɽ誮": "ʫ繕ȫ",
|
||||||
|
"ŚB碠k9": "i磊ůď逳鞪?3)藵睋邔\u0026Ű"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"activedirectory": {
|
"activedirectory": {
|
||||||
"userDN": "伒犘c钡ɏȫ"
|
"userDN": "s",
|
||||||
|
"extraRefreshAttributes": {
|
||||||
|
"ƉǢIȽ齤士bEǎ儯惝IozŁ5rƖ螼": "偶宾儮猷V麹Œ颛Ė應,Ɣ鬅X¤"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requestedAudience": [
|
"requestedAudience": [
|
||||||
"š%OpKȱ藚ɏ¬Ê蒭堜",
|
"tO灞浛a齙\\蹼偦歛ơ",
|
||||||
"ɽ誮rʨ鷞aŚB碠k",
|
"皦pSǬŝ社Vƅȭǝ*"
|
||||||
"Ċi磊ůď逳鞪?3)藵睋"
|
|
||||||
],
|
],
|
||||||
"grantedAudience": [
|
"grantedAudience": [
|
||||||
"\u0026Ű惫蜀Ģ¡圔",
|
"ĝ\"zvưã置bņ抰蛖a³2ʫ",
|
||||||
"墀jMʥ",
|
"Ŷɽ蔒PR}Ųʓl{鼐jÃ轘屔挝",
|
||||||
"+î艔垎0"
|
"Œų崓ļ憽-蹐È_¸]fś"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "2"
|
"version": "2"
|
||||||
|
@ -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 authorizationcode
|
package authorizationcode
|
||||||
@ -28,10 +28,10 @@ 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/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
kubetesting "k8s.io/client-go/testing"
|
kubetesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/fositestorage"
|
"go.pinniped.dev/internal/fositestorage"
|
||||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||||
@ -65,7 +65,7 @@ func TestAuthorizationCodeStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"active":true,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
@ -84,7 +84,7 @@ func TestAuthorizationCodeStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"active":false,"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/authcode",
|
Type: "storage.pinniped.dev/authcode",
|
||||||
@ -258,7 +258,7 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
|||||||
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, oauth2.AuthorizeCodeStorage) {
|
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, oauth2.AuthorizeCodeStorage) {
|
||||||
client := fake.NewSimpleClientset()
|
client := fake.NewSimpleClientset()
|
||||||
secrets := client.CoreV1().Secrets(namespace)
|
secrets := client.CoreV1().Secrets(namespace)
|
||||||
return context.Background(), client, secrets, New(secrets, clock.NewFakeClock(fakeNow).Now, lifetime)
|
return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession asserts that we can correctly round trip our authorize code session.
|
// TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession asserts that we can correctly round trip our authorize code session.
|
||||||
@ -398,7 +398,7 @@ func TestFuzzAndJSONNewValidEmptyAuthorizeCodeSession(t *testing.T) {
|
|||||||
// while the fuzzer will panic if AuthorizeRequest changes in a way that cannot be fuzzed,
|
// while the fuzzer will panic if AuthorizeRequest changes in a way that cannot be fuzzed,
|
||||||
// if it adds a new field that can be fuzzed, this check will fail
|
// if it adds a new field that can be fuzzed, this check will fail
|
||||||
// thus if AuthorizeRequest changes, we will detect it here (though we could possibly miss an omitempty field)
|
// thus if AuthorizeRequest changes, we will detect it here (though we could possibly miss an omitempty field)
|
||||||
require.JSONEq(t, ExpectedAuthorizeCodeSessionJSONFromFuzzing, authorizeCodeSessionJSONFromFuzzing)
|
require.JSONEq(t, ExpectedAuthorizeCodeSessionJSONFromFuzzing, authorizeCodeSessionJSONFromFuzzing, "actual:\n%s", authorizeCodeSessionJSONFromFuzzing)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadFromSecret(t *testing.T) {
|
func TestReadFromSecret(t *testing.T) {
|
||||||
|
@ -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 openidconnect
|
package openidconnect
|
||||||
@ -16,10 +16,10 @@ import (
|
|||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
@ -52,7 +52,7 @@ func TestOpenIdConnectStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/oidc",
|
Type: "storage.pinniped.dev/oidc",
|
||||||
@ -100,7 +100,7 @@ func TestOpenIdConnectStorage(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, request, newRequest)
|
require.Equal(t, request, newRequest)
|
||||||
|
|
||||||
err = storage.DeleteOpenIDConnectSession(ctx, "fancy-code.fancy-signature")
|
err = storage.DeleteOpenIDConnectSession(ctx, "fancy-code.fancy-signature") //nolint: staticcheck // we know this is deprecated and never called. our GC controller cleans these up.
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
|
testutil.LogActualJSONFromCreateAction(t, client, 0) // makes it easier to update expected values when needed
|
||||||
@ -200,5 +200,5 @@ func TestAuthcodeHasNoDot(t *testing.T) {
|
|||||||
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, openid.OpenIDConnectRequestStorage) {
|
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, openid.OpenIDConnectRequestStorage) {
|
||||||
client := fake.NewSimpleClientset()
|
client := fake.NewSimpleClientset()
|
||||||
secrets := client.CoreV1().Secrets(namespace)
|
secrets := client.CoreV1().Secrets(namespace)
|
||||||
return context.Background(), client, secrets, New(secrets, clock.NewFakeClock(fakeNow).Now, lifetime)
|
return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime)
|
||||||
}
|
}
|
||||||
|
@ -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 pkce
|
package pkce
|
||||||
@ -16,10 +16,10 @@ import (
|
|||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
@ -52,7 +52,7 @@ func TestPKCEStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/pkce",
|
Type: "storage.pinniped.dev/pkce",
|
||||||
@ -199,5 +199,5 @@ func TestCreateWithWrongRequesterDataTypes(t *testing.T) {
|
|||||||
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, pkce.PKCERequestStorage) {
|
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, pkce.PKCERequestStorage) {
|
||||||
client := fake.NewSimpleClientset()
|
client := fake.NewSimpleClientset()
|
||||||
secrets := client.CoreV1().Secrets(namespace)
|
secrets := client.CoreV1().Secrets(namespace)
|
||||||
return context.Background(), client, secrets, New(secrets, clock.NewFakeClock(fakeNow).Now, lifetime)
|
return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime)
|
||||||
}
|
}
|
||||||
|
@ -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 refreshtoken
|
package refreshtoken
|
||||||
@ -16,10 +16,10 @@ import (
|
|||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
clocktesting "k8s.io/utils/clock/testing"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
@ -52,7 +52,7 @@ func TestRefreshTokenStorage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
@ -122,7 +122,7 @@ func TestRefreshTokenStorageRevocation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":""}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
"pinniped-storage-data": []byte(`{"request":{"id":"abcd-1","requestedAt":"0001-01-01T00:00:00Z","client":{"id":"pinny","redirect_uris":null,"grant_types":null,"response_types":null,"scopes":null,"audience":null,"public":true,"jwks_uri":"where","jwks":null,"token_endpoint_auth_method":"something","request_uris":null,"request_object_signing_alg":"","token_endpoint_auth_signing_alg":""},"scopes":null,"grantedScopes":null,"form":{"key":["val"]},"session":{"fosite":{"Claims":null,"Headers":null,"ExpiresAt":null,"Username":"snorlax","Subject":"panda"},"custom":{"providerUID":"fake-provider-uid","providerName":"fake-provider-name","providerType":"fake-provider-type","oidc":{"upstreamRefreshToken":"fake-upstream-refresh-token","upstreamAccessToken":"","upstreamSubject":"some-subject","upstreamIssuer":"some-issuer"}}},"requestedAudience":null,"grantedAudience":null},"version":"2"}`),
|
||||||
"pinniped-storage-version": []byte("1"),
|
"pinniped-storage-version": []byte("1"),
|
||||||
},
|
},
|
||||||
Type: "storage.pinniped.dev/refresh-token",
|
Type: "storage.pinniped.dev/refresh-token",
|
||||||
@ -276,7 +276,7 @@ func TestCreateWithoutRequesterID(t *testing.T) {
|
|||||||
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) {
|
func makeTestSubject() (context.Context, *fake.Clientset, corev1client.SecretInterface, RevocationStorage) {
|
||||||
client := fake.NewSimpleClientset()
|
client := fake.NewSimpleClientset()
|
||||||
secrets := client.CoreV1().Secrets(namespace)
|
secrets := client.CoreV1().Secrets(namespace)
|
||||||
return context.Background(), client, secrets, New(secrets, clock.NewFakeClock(fakeNow).Now, lifetime)
|
return context.Background(), client, secrets, New(secrets, clocktesting.NewFakeClock(fakeNow).Now, lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadFromSecret(t *testing.T) {
|
func TestReadFromSecret(t *testing.T) {
|
||||||
|
@ -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 kubeclient
|
package kubeclient
|
||||||
@ -8,8 +8,6 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
@ -155,7 +153,7 @@ func createSecureKubeConfig(kubeConfig *restclient.Config) (*restclient.Config,
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
tlsConfig, err := netTLSClientConfig(rt)
|
tlsConfig, err := net.TLSClientConfig(rt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// this assumes none of our production code calls Wrap or messes with WrapTransport.
|
// this assumes none of our production code calls Wrap or messes with WrapTransport.
|
||||||
// this is a reasonable assumption because all such code should live in this package
|
// this is a reasonable assumption because all such code should live in this package
|
||||||
@ -205,7 +203,7 @@ func AssertSecureConfig(kubeConfig *restclient.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AssertSecureTransport(rt http.RoundTripper) error {
|
func AssertSecureTransport(rt http.RoundTripper) error {
|
||||||
tlsConfig, err := netTLSClientConfig(rt)
|
tlsConfig, err := net.TLSClientConfig(rt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get TLS config: %w", err)
|
return fmt.Errorf("failed to get TLS config: %w", err)
|
||||||
}
|
}
|
||||||
@ -224,33 +222,6 @@ func AssertSecureTransport(rt http.RoundTripper) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func netTLSClientConfig(rt http.RoundTripper) (*tls.Config, error) {
|
|
||||||
tlsConfig, err := net.TLSClientConfig(rt)
|
|
||||||
if err == nil {
|
|
||||||
return tlsConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106014
|
|
||||||
if err.Error() == "unknown transport type: *exec.roundTripper" {
|
|
||||||
return net.TLSClientConfig(extractRTUnsafe(rt))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractRTUnsafe(rt http.RoundTripper) (out http.RoundTripper) {
|
|
||||||
for wrapper, ok := rt.(net.RoundTripperWrapper); ok; wrapper, ok = rt.(net.RoundTripperWrapper) {
|
|
||||||
// keep peeling the wrappers until we get to the exec.roundTripper
|
|
||||||
rt = wrapper.WrappedRoundTripper()
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is some dark magic to read a private field
|
|
||||||
baseField := reflect.ValueOf(rt).Elem().FieldByName("base")
|
|
||||||
basePointer := (*http.RoundTripper)(unsafe.Pointer(baseField.UnsafeAddr()))
|
|
||||||
|
|
||||||
return *basePointer
|
|
||||||
}
|
|
||||||
|
|
||||||
func Secure(config *restclient.Config) (kubernetes.Interface, *restclient.Config, error) {
|
func Secure(config *restclient.Config) (kubernetes.Interface, *restclient.Config, error) {
|
||||||
// our middleware does not apply to the returned restclient.Config, therefore, this
|
// our middleware does not apply to the returned restclient.Config, therefore, this
|
||||||
// client not having a leader election lock is irrelevant since it would not be enforced
|
// client not having a leader election lock is irrelevant since it would not be enforced
|
||||||
|
@ -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 kubeclient
|
package kubeclient
|
||||||
@ -19,6 +19,7 @@ import (
|
|||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/net"
|
||||||
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
|
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
@ -1109,7 +1110,7 @@ func testUnwrap(t *testing.T, client *Client, serverSubjects [][]byte) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel() // make sure to run in parallel to confirm that our client-go TLS cache busting works (i.e. assert no data races)
|
t.Parallel() // make sure to run in parallel to confirm that our client-go TLS cache busting works (i.e. assert no data races)
|
||||||
|
|
||||||
tlsConfig, err := netTLSClientConfig(tt.rt)
|
tlsConfig, err := net.TLSClientConfig(tt.rt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, tlsConfig)
|
require.NotNil(t, tlsConfig)
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
|
@ -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
|
||||||
//
|
//
|
||||||
|
|
||||||
@ -186,6 +186,20 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetUsernameClaim() *gom
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsernameClaim", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetUsernameClaim))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsernameClaim", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetUsernameClaim))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasUserInfoURL mocks base method.
|
||||||
|
func (m *MockUpstreamOIDCIdentityProviderI) HasUserInfoURL() bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "HasUserInfoURL")
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasUserInfoURL indicates an expected call of HasUserInfoURL.
|
||||||
|
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) HasUserInfoURL() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasUserInfoURL", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).HasUserInfoURL))
|
||||||
|
}
|
||||||
|
|
||||||
// PasswordCredentialsGrantAndValidateTokens mocks base method.
|
// PasswordCredentialsGrantAndValidateTokens mocks base method.
|
||||||
func (m *MockUpstreamOIDCIdentityProviderI) PasswordCredentialsGrantAndValidateTokens(arg0 context.Context, arg1, arg2 string) (*oidctypes.Token, error) {
|
func (m *MockUpstreamOIDCIdentityProviderI) PasswordCredentialsGrantAndValidateTokens(arg0 context.Context, arg1, arg2 string) (*oidctypes.Token, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -230,17 +244,17 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) RevokeToken(arg0, arg1,
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeToken", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).RevokeToken), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeToken", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).RevokeToken), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateToken mocks base method.
|
// ValidateTokenAndMergeWithUserInfo mocks base method.
|
||||||
func (m *MockUpstreamOIDCIdentityProviderI) ValidateToken(arg0 context.Context, arg1 *oauth2.Token, arg2 nonce.Nonce) (*oidctypes.Token, error) {
|
func (m *MockUpstreamOIDCIdentityProviderI) ValidateTokenAndMergeWithUserInfo(arg0 context.Context, arg1 *oauth2.Token, arg2 nonce.Nonce, arg3, arg4 bool) (*oidctypes.Token, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ValidateToken", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "ValidateTokenAndMergeWithUserInfo", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(*oidctypes.Token)
|
ret0, _ := ret[0].(*oidctypes.Token)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateToken indicates an expected call of ValidateToken.
|
// ValidateTokenAndMergeWithUserInfo indicates an expected call of ValidateTokenAndMergeWithUserInfo.
|
||||||
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) ValidateToken(arg0, arg1, arg2 interface{}) *gomock.Call {
|
func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) ValidateTokenAndMergeWithUserInfo(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateToken", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).ValidateToken), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateTokenAndMergeWithUserInfo", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).ValidateTokenAndMergeWithUserInfo), arg0, arg1, arg2, arg3, arg4)
|
||||||
}
|
}
|
||||||
|
@ -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 auth provides a handler for the OIDC authorization endpoint.
|
// Package auth provides a handler for the OIDC authorization endpoint.
|
||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"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"
|
||||||
@ -89,7 +90,7 @@ func handleAuthRequestForLDAPUpstream(
|
|||||||
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
ldapUpstream provider.UpstreamLDAPIdentityProviderI,
|
||||||
idpType psession.ProviderType,
|
idpType psession.ProviderType,
|
||||||
) error {
|
) error {
|
||||||
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper)
|
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, true)
|
||||||
if !created {
|
if !created {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -106,7 +107,7 @@ func handleAuthRequestForLDAPUpstream(
|
|||||||
}
|
}
|
||||||
if !authenticated {
|
if !authenticated {
|
||||||
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."))
|
fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider."), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
||||||
@ -122,12 +123,14 @@ func handleAuthRequestForLDAPUpstream(
|
|||||||
|
|
||||||
if idpType == psession.ProviderTypeLDAP {
|
if idpType == psession.ProviderTypeLDAP {
|
||||||
customSessionData.LDAP = &psession.LDAPSessionData{
|
customSessionData.LDAP = &psession.LDAPSessionData{
|
||||||
UserDN: dn,
|
UserDN: dn,
|
||||||
|
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if idpType == psession.ProviderTypeActiveDirectory {
|
if idpType == psession.ProviderTypeActiveDirectory {
|
||||||
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
|
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
|
||||||
UserDN: dn,
|
UserDN: dn,
|
||||||
|
ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +144,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
|
|||||||
oauthHelper fosite.OAuth2Provider,
|
oauthHelper fosite.OAuth2Provider,
|
||||||
oidcUpstream provider.UpstreamOIDCIdentityProviderI,
|
oidcUpstream provider.UpstreamOIDCIdentityProviderI,
|
||||||
) error {
|
) error {
|
||||||
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper)
|
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, true)
|
||||||
if !created {
|
if !created {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -155,7 +158,7 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
|
|||||||
// 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,
|
return 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."))
|
"Resource owner password credentials grant is not allowed for this upstream provider according to its configuration."), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password)
|
token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(r.Context(), username, password)
|
||||||
@ -168,35 +171,24 @@ func handleAuthRequestForOIDCUpstreamPasswordGrant(
|
|||||||
// 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,
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
fosite.ErrAccessDenied.WithDebug(err.Error())) // WithDebug hides the error from the client
|
fosite.ErrAccessDenied.WithDebug(err.Error()), true) // WithDebug hides the error from the client
|
||||||
}
|
|
||||||
|
|
||||||
if token.RefreshToken == nil || token.RefreshToken.Token == "" {
|
|
||||||
plog.Warning("refresh token not returned by upstream provider during password grant, "+
|
|
||||||
"please check configuration of OIDCIdentityProvider and the client in the upstream provider's API/UI",
|
|
||||||
"upstreamName", oidcUpstream.GetName(),
|
|
||||||
"scopes", oidcUpstream.GetScopes())
|
|
||||||
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
|
|
||||||
fosite.ErrAccessDenied.WithHint(
|
|
||||||
"Refresh token not returned by upstream provider during password grant."))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()),
|
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
customSessionData := &psession.CustomSessionData{
|
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token)
|
||||||
ProviderUID: oidcUpstream.GetResourceUID(),
|
if err != nil {
|
||||||
ProviderName: oidcUpstream.GetName(),
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), true,
|
||||||
OIDC: &psession.OIDCSessionData{
|
)
|
||||||
UpstreamRefreshToken: token.RefreshToken.Token,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
|
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +204,7 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
|
|||||||
upstreamStateEncoder oidc.Encoder,
|
upstreamStateEncoder oidc.Encoder,
|
||||||
cookieCodec oidc.Codec,
|
cookieCodec oidc.Codec,
|
||||||
) error {
|
) error {
|
||||||
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper)
|
authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper, false)
|
||||||
if !created {
|
if !created {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -229,7 +221,7 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
|
csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE)
|
||||||
@ -272,7 +264,7 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
|
|||||||
|
|
||||||
promptParam := r.Form.Get(promptParamName)
|
promptParam := r.Form.Get(promptParamName)
|
||||||
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
|
if promptParam == promptParamNone && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) {
|
||||||
return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired)
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester, fosite.ErrLoginRequired, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() {
|
for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() {
|
||||||
@ -293,13 +285,13 @@ func handleAuthRequestForOIDCUpstreamAuthcodeGrant(
|
|||||||
encodedStateParamValue,
|
encodedStateParamValue,
|
||||||
authCodeOptions...,
|
authCodeOptions...,
|
||||||
),
|
),
|
||||||
302,
|
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error) error {
|
func writeAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, err error, isBrowserless bool) error {
|
||||||
if plog.Enabled(plog.LevelTrace) {
|
if plog.Enabled(plog.LevelTrace) {
|
||||||
// When trace level logging is enabled, include the stack trace in the log message.
|
// When trace level logging is enabled, include the stack trace in the log message.
|
||||||
keysAndValues := oidc.FositeErrorForLog(err)
|
keysAndValues := oidc.FositeErrorForLog(err)
|
||||||
@ -312,6 +304,9 @@ func writeAuthorizeError(w http.ResponseWriter, oauthHelper fosite.OAuth2Provide
|
|||||||
} else {
|
} else {
|
||||||
plog.Info("authorize response error", oidc.FositeErrorForLog(err)...)
|
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).
|
// Return an error according to OIDC spec 3.1.2.6 (second paragraph).
|
||||||
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
oauthHelper.WriteAuthorizeError(w, authorizeRequester, err)
|
||||||
return nil
|
return nil
|
||||||
@ -331,29 +326,53 @@ func makeDownstreamSessionAndReturnAuthcodeRedirect(
|
|||||||
|
|
||||||
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err)
|
return writeAuthorizeError(w, oauthHelper, authorizeRequester, err, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w = rewriteStatusSeeOtherToStatusFoundForBrowserless(w)
|
||||||
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
|
oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder)
|
||||||
|
|
||||||
return nil
|
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,
|
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester,
|
||||||
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."))
|
fosite.ErrAccessDenied.WithHintf("Missing or blank username or password."), true)
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
return username, password, true
|
return username, password, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider) (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)
|
_ = writeAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless)
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 auth
|
package auth
|
||||||
@ -56,6 +56,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
oidcUpstreamUsernameClaim = "the-user-claim"
|
oidcUpstreamUsernameClaim = "the-user-claim"
|
||||||
oidcUpstreamGroupsClaim = "the-groups-claim"
|
oidcUpstreamGroupsClaim = "the-groups-claim"
|
||||||
oidcPasswordGrantUpstreamRefreshToken = "some-opaque-token" //nolint: gosec
|
oidcPasswordGrantUpstreamRefreshToken = "some-opaque-token" //nolint: gosec
|
||||||
|
oidcUpstreamAccessToken = "some-access-token"
|
||||||
|
|
||||||
downstreamIssuer = "https://my-downstream-issuer.com/some-path"
|
downstreamIssuer = "https://my-downstream-issuer.com/some-path"
|
||||||
downstreamRedirectURI = "http://127.0.0.1/callback"
|
downstreamRedirectURI = "http://127.0.0.1/callback"
|
||||||
@ -154,9 +155,15 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
"state": happyState,
|
"state": happyState,
|
||||||
}
|
}
|
||||||
|
|
||||||
fositeAccessDeniedWithMissingRefreshTokenErrorQuery = map[string]string{
|
fositeAccessDeniedWithMissingAccessTokenErrorQuery = map[string]string{
|
||||||
"error": "access_denied",
|
"error": "access_denied",
|
||||||
"error_description": "The resource owner or authorization server denied the request. Refresh token not returned by upstream provider during password grant.",
|
"error_description": "The resource owner or authorization server denied the request. Reason: neither access token nor refresh token returned by upstream provider.",
|
||||||
|
"state": happyState,
|
||||||
|
}
|
||||||
|
|
||||||
|
fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery = map[string]string{
|
||||||
|
"error": "access_denied",
|
||||||
|
"error_description": "The resource owner or authorization server denied the request. Reason: access token was returned by upstream provider but there was no userinfo endpoint.",
|
||||||
"state": happyState,
|
"state": happyState,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,6 +276,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
happyLDAPUID := "some-ldap-uid"
|
happyLDAPUID := "some-ldap-uid"
|
||||||
happyLDAPUserDN := "cn=foo,dn=bar"
|
happyLDAPUserDN := "cn=foo,dn=bar"
|
||||||
happyLDAPGroups := []string{"group1", "group2", "group3"}
|
happyLDAPGroups := []string{"group1", "group2", "group3"}
|
||||||
|
happyLDAPExtraRefreshAttribute := "some-refresh-attribute"
|
||||||
|
happyLDAPExtraRefreshValue := "some-refresh-attribute-value"
|
||||||
|
|
||||||
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
|
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -285,6 +294,9 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
Groups: happyLDAPGroups,
|
Groups: happyLDAPGroups,
|
||||||
},
|
},
|
||||||
DN: happyLDAPUserDN,
|
DN: happyLDAPUserDN,
|
||||||
|
ExtraRefreshAttributes: map[string]string{
|
||||||
|
happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue,
|
||||||
|
},
|
||||||
}, true, nil
|
}, true, nil
|
||||||
}
|
}
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
@ -442,7 +454,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
OIDC: nil,
|
OIDC: nil,
|
||||||
LDAP: nil,
|
LDAP: nil,
|
||||||
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||||
UserDN: happyLDAPUserDN,
|
UserDN: happyLDAPUserDN,
|
||||||
|
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,7 +465,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
ProviderType: psession.ProviderTypeLDAP,
|
ProviderType: psession.ProviderTypeLDAP,
|
||||||
OIDC: nil,
|
OIDC: nil,
|
||||||
LDAP: &psession.LDAPSessionData{
|
LDAP: &psession.LDAPSessionData{
|
||||||
UserDN: happyLDAPUserDN,
|
UserDN: happyLDAPUserDN,
|
||||||
|
ExtraRefreshAttributes: map[string]string{happyLDAPExtraRefreshAttribute: happyLDAPExtraRefreshValue},
|
||||||
},
|
},
|
||||||
ActiveDirectory: nil,
|
ActiveDirectory: nil,
|
||||||
}
|
}
|
||||||
@ -463,6 +477,19 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
OIDC: &psession.OIDCSessionData{
|
OIDC: &psession.OIDCSessionData{
|
||||||
UpstreamRefreshToken: oidcPasswordGrantUpstreamRefreshToken,
|
UpstreamRefreshToken: oidcPasswordGrantUpstreamRefreshToken,
|
||||||
|
UpstreamSubject: oidcUpstreamSubject,
|
||||||
|
UpstreamIssuer: oidcUpstreamIssuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken := &psession.CustomSessionData{
|
||||||
|
ProviderUID: oidcPasswordGrantUpstreamResourceUID,
|
||||||
|
ProviderName: oidcPasswordGrantUpstreamName,
|
||||||
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
|
OIDC: &psession.OIDCSessionData{
|
||||||
|
UpstreamAccessToken: oidcUpstreamAccessToken,
|
||||||
|
UpstreamSubject: oidcUpstreamSubject,
|
||||||
|
UpstreamIssuer: oidcUpstreamIssuer,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -526,7 +553,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: happyGetRequestPath,
|
path: happyGetRequestPath,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), nil),
|
||||||
@ -608,7 +635,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: happyGetRequestPath,
|
path: happyGetRequestPath,
|
||||||
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
|
csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ",
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil),
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), nil),
|
||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
@ -626,7 +653,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
path: "/some/path",
|
path: "/some/path",
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
body: encodeQuery(happyGetRequestQueryMap),
|
body: encodeQuery(happyGetRequestQueryMap),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "",
|
wantContentType: "",
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
@ -715,7 +742,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
body: encodeQuery(happyGetRequestQueryMap),
|
body: encodeQuery(happyGetRequestQueryMap),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
@ -734,7 +761,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "login"}),
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
body: encodeQuery(happyGetRequestQueryMap),
|
body: encodeQuery(happyGetRequestQueryMap),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
@ -753,7 +780,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none"}),
|
||||||
contentType: "application/x-www-form-urlencoded",
|
contentType: "application/x-www-form-urlencoded",
|
||||||
body: encodeQuery(happyGetRequestQueryMap),
|
body: encodeQuery(happyGetRequestQueryMap),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeLoginRequiredErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -769,7 +796,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: happyGetRequestPath,
|
path: happyGetRequestPath,
|
||||||
csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped",
|
csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped",
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
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,
|
||||||
@ -789,7 +816,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
path: modifiedHappyGetRequestPath(map[string]string{
|
path: modifiedHappyGetRequestPath(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
|
||||||
}),
|
}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
||||||
@ -855,7 +882,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid offline_access"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid offline_access"}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
||||||
@ -864,6 +891,50 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantUpstreamStateParamInLocationHeader: true,
|
wantUpstreamStateParamInLocationHeader: true,
|
||||||
wantBodyStringWithLocationInHref: true,
|
wantBodyStringWithLocationInHref: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "OIDC password grant happy path when upstream IDP returned empty refresh token but it did return an access token and has a userinfo endpoint",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithUserInfoURL().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: happyGetRequestPath,
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
|
||||||
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OIDC password grant happy path when upstream IDP did not return a refresh token but it did return an access token and has a userinfo endpoint",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithUserInfoURL().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: happyGetRequestPath,
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
|
||||||
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
|
wantDownstreamRedirectURI: downstreamRedirectURI,
|
||||||
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: expectedHappyOIDCPasswordGrantCustomSessionWithAccessToken,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "error during upstream LDAP authentication",
|
name: "error during upstream LDAP authentication",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider),
|
||||||
@ -1006,8 +1077,8 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "return an error when upstream IDP did not return a refresh token",
|
name: "password grant returns an error when upstream IDP returns no refresh token with an access token but has no userinfo endpoint",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithoutUserInfoURL().Build()),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: happyGetRequestPath,
|
path: happyGetRequestPath,
|
||||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
@ -1015,12 +1086,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingRefreshTokenErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "return an error when upstream IDP did not return a refresh token",
|
name: "password grant returns an error when upstream IDP returns empty refresh token with an access token but has no userinfo endpoint",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithoutUserInfoURL().Build()),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: happyGetRequestPath,
|
path: happyGetRequestPath,
|
||||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
@ -1028,7 +1099,59 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusFound,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingRefreshTokenErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUserInfoEndpointErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password grant returns an error when upstream IDP returns empty refresh token and empty access token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithEmptyAccessToken().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: happyGetRequestPath,
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password grant returns an error when upstream IDP returns no refresh and no access token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithoutAccessToken().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: happyGetRequestPath,
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password grant returns an error when upstream IDP returns no refresh token and empty access token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithEmptyAccessToken().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: happyGetRequestPath,
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
|
||||||
|
wantBodyString: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password grant returns an error when upstream IDP returns empty refresh token and no access token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithoutAccessToken().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: happyGetRequestPath,
|
||||||
|
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||||
|
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||||
|
wantPasswordGrantCall: happyUpstreamPasswordGrantMockExpectation,
|
||||||
|
wantStatus: http.StatusFound,
|
||||||
|
wantContentType: "application/json; charset=utf-8",
|
||||||
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingAccessTokenErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1163,7 +1286,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1210,7 +1333,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
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.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1261,7 +1384,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1342,7 +1465,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
|
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1384,7 +1507,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
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.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1426,7 +1549,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
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.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1468,7 +1591,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
|
path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1512,7 +1635,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -1559,7 +1682,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
|
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantCSRFValueInCookieHeader: happyCSRF,
|
wantCSRFValueInCookieHeader: happyCSRF,
|
||||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(
|
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(
|
||||||
@ -2042,7 +2165,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
cookieEncoder: happyCookieEncoder,
|
cookieEncoder: happyCookieEncoder,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}),
|
path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}),
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantContentType: "application/json; charset=utf-8",
|
wantContentType: "application/json; charset=utf-8",
|
||||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
|
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
|
||||||
wantBodyString: "",
|
wantBodyString: "",
|
||||||
@ -2301,8 +2424,16 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
|||||||
case test.wantBodyJSON != "":
|
case test.wantBodyJSON != "":
|
||||||
require.JSONEq(t, test.wantBodyJSON, rsp.Body.String())
|
require.JSONEq(t, test.wantBodyJSON, rsp.Body.String())
|
||||||
case test.wantBodyStringWithLocationInHref:
|
case test.wantBodyStringWithLocationInHref:
|
||||||
anchorTagWithLocationHref := fmt.Sprintf("<a href=\"%s\">Found</a>.\n\n", html.EscapeString(actualLocation))
|
switch code := rsp.Code; code {
|
||||||
require.Equal(t, anchorTagWithLocationHref, rsp.Body.String())
|
case http.StatusFound:
|
||||||
|
anchorTagWithLocationHref := fmt.Sprintf("<a href=\"%s\">Found</a>.\n\n", html.EscapeString(actualLocation))
|
||||||
|
require.Equal(t, anchorTagWithLocationHref, rsp.Body.String())
|
||||||
|
case http.StatusSeeOther:
|
||||||
|
anchorTagWithLocationHref := fmt.Sprintf("<a href=\"%s\">See Other</a>.\n\n", html.EscapeString(actualLocation))
|
||||||
|
require.Equal(t, anchorTagWithLocationHref, rsp.Body.String())
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected response code: %v", code)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
require.Equal(t, test.wantBodyString, rsp.Body.String())
|
require.Equal(t, test.wantBodyString, rsp.Body.String())
|
||||||
}
|
}
|
||||||
|
@ -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 callback provides a handler for the OIDC callback endpoint.
|
// Package callback provides a handler for the OIDC callback endpoint.
|
||||||
@ -19,7 +19,6 @@ import (
|
|||||||
"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/plog"
|
||||||
"go.pinniped.dev/internal/psession"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewHandler(
|
func NewHandler(
|
||||||
@ -69,28 +68,17 @@ func NewHandler(
|
|||||||
return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens")
|
return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens")
|
||||||
}
|
}
|
||||||
|
|
||||||
if token.RefreshToken == nil || token.RefreshToken.Token == "" {
|
|
||||||
plog.Warning("refresh token not returned by upstream provider during authcode exchange, "+
|
|
||||||
"please check configuration of OIDCIdentityProvider and the client in the upstream provider's API/UI",
|
|
||||||
"upstreamName", upstreamIDPConfig.GetName(),
|
|
||||||
"scopes", upstreamIDPConfig.GetScopes(),
|
|
||||||
"additionalParams", upstreamIDPConfig.GetAdditionalAuthcodeParams())
|
|
||||||
return httperr.New(http.StatusUnprocessableEntity, "refresh token not returned by upstream provider during authcode exchange")
|
|
||||||
}
|
|
||||||
|
|
||||||
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
|
subject, username, groups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, &psession.CustomSessionData{
|
customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(upstreamIDPConfig, token)
|
||||||
ProviderUID: upstreamIDPConfig.GetResourceUID(),
|
if err != nil {
|
||||||
ProviderName: upstreamIDPConfig.GetName(),
|
return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err)
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
}
|
||||||
OIDC: &psession.OIDCSessionData{
|
|
||||||
UpstreamRefreshToken: token.RefreshToken.Token,
|
openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, customSessionData)
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -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 callback
|
package callback
|
||||||
@ -31,6 +31,7 @@ const (
|
|||||||
|
|
||||||
oidcUpstreamIssuer = "https://my-upstream-issuer.com"
|
oidcUpstreamIssuer = "https://my-upstream-issuer.com"
|
||||||
oidcUpstreamRefreshToken = "test-refresh-token"
|
oidcUpstreamRefreshToken = "test-refresh-token"
|
||||||
|
oidcUpstreamAccessToken = "test-access-token"
|
||||||
oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
|
oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
|
||||||
oidcUpstreamSubjectQueryEscaped = "abc123-some+guid"
|
oidcUpstreamSubjectQueryEscaped = "abc123-some+guid"
|
||||||
oidcUpstreamUsername = "test-pinniped-username"
|
oidcUpstreamUsername = "test-pinniped-username"
|
||||||
@ -77,7 +78,21 @@ var (
|
|||||||
ProviderUID: happyUpstreamIDPResourceUID,
|
ProviderUID: happyUpstreamIDPResourceUID,
|
||||||
ProviderName: happyUpstreamIDPName,
|
ProviderName: happyUpstreamIDPName,
|
||||||
ProviderType: psession.ProviderTypeOIDC,
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
OIDC: &psession.OIDCSessionData{UpstreamRefreshToken: oidcUpstreamRefreshToken},
|
OIDC: &psession.OIDCSessionData{
|
||||||
|
UpstreamRefreshToken: oidcUpstreamRefreshToken,
|
||||||
|
UpstreamIssuer: oidcUpstreamIssuer,
|
||||||
|
UpstreamSubject: oidcUpstreamSubject,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
happyDownstreamAccessTokenCustomSessionData = &psession.CustomSessionData{
|
||||||
|
ProviderUID: happyUpstreamIDPResourceUID,
|
||||||
|
ProviderName: happyUpstreamIDPName,
|
||||||
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
|
OIDC: &psession.OIDCSessionData{
|
||||||
|
UpstreamAccessToken: oidcUpstreamAccessToken,
|
||||||
|
UpstreamIssuer: oidcUpstreamIssuer,
|
||||||
|
UpstreamSubject: oidcUpstreamSubject,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -174,12 +189,12 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "GET with good state and cookie and successful upstream token exchange returns 302 to downstream client callback with its state and code",
|
name: "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -196,6 +211,29 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "GET with authcode exchange that returns an access token but no refresh token when there is a userinfo endpoint returns 303 to downstream client callback with its state and code",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithUserInfoURL().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusSeeOther,
|
||||||
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
|
wantBody: "",
|
||||||
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
|
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
|
||||||
|
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
|
||||||
|
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
|
||||||
|
wantDownstreamNonce: downstreamNonce,
|
||||||
|
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
|
||||||
|
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
|
||||||
|
wantDownstreamCustomSessionData: happyDownstreamAccessTokenCustomSessionData,
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups",
|
name: "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
@ -204,7 +242,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -229,7 +267,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -256,7 +294,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -284,7 +322,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound, // succeed despite `email_verified=false` because we're not using the email claim for anything
|
wantStatus: http.StatusSeeOther, // succeed despite `email_verified=false` because we're not using the email claim for anything
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -319,28 +357,70 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "return an error when upstream IDP did not return a refresh token",
|
name: "return an error when upstream IDP returned no refresh token with an access token when there is no userinfo endpoint",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken).WithoutUserInfoURL().Build()),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusUnprocessableEntity,
|
wantStatus: http.StatusUnprocessableEntity,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBody: "Unprocessable Entity: refresh token not returned by upstream provider during authcode exchange\n",
|
wantBody: "Unprocessable Entity: access token was returned by upstream provider but there was no userinfo endpoint\n",
|
||||||
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
performedByUpstreamName: happyUpstreamIDPName,
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "return an error when upstream IDP returned an empty refresh token",
|
name: "return an error when upstream IDP returned no refresh token and no access token",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithoutAccessToken().Build()),
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusUnprocessableEntity,
|
wantStatus: http.StatusUnprocessableEntity,
|
||||||
wantContentType: htmlContentType,
|
wantContentType: htmlContentType,
|
||||||
wantBody: "Unprocessable Entity: refresh token not returned by upstream provider during authcode exchange\n",
|
wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n",
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return an error when upstream IDP returned an empty refresh token and empty access token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithEmptyAccessToken().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusUnprocessableEntity,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n",
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return an error when upstream IDP returned no refresh token and empty access token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithEmptyAccessToken().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusUnprocessableEntity,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n",
|
||||||
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return an error when upstream IDP returned an empty refresh token and no access token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithoutAccessToken().Build()),
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
|
csrfCookie: happyCSRFCookie,
|
||||||
|
wantStatus: http.StatusUnprocessableEntity,
|
||||||
|
wantContentType: htmlContentType,
|
||||||
|
wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n",
|
||||||
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
|
||||||
performedByUpstreamName: happyUpstreamIDPName,
|
performedByUpstreamName: happyUpstreamIDPName,
|
||||||
args: happyExchangeAndValidateTokensArgs,
|
args: happyExchangeAndValidateTokensArgs,
|
||||||
@ -372,7 +452,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -397,7 +477,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -422,7 +502,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -575,7 +655,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -601,7 +681,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
Build(t, happyStateCodec),
|
Build(t, happyStateCodec),
|
||||||
).String(),
|
).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access&state=` + happyDownstreamState,
|
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access&state=` + happyDownstreamState,
|
||||||
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
@ -698,7 +778,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: newRequestPath().WithState(happyState).String(),
|
path: newRequestPath().WithState(happyState).String(),
|
||||||
csrfCookie: happyCSRFCookie,
|
csrfCookie: happyCSRFCookie,
|
||||||
wantStatus: http.StatusFound,
|
wantStatus: http.StatusSeeOther,
|
||||||
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
|
||||||
wantBody: "",
|
wantBody: "",
|
||||||
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
// 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 downstreamsession provides some shared helpers for creating downstream OIDC sessions.
|
// Package downstreamsession provides some shared helpers for creating downstream OIDC sessions.
|
||||||
package downstreamsession
|
package downstreamsession
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
"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"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -58,6 +60,55 @@ func MakeDownstreamSession(subject string, username string, groups []string, cus
|
|||||||
return openIDSession
|
return openIDSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MakeDownstreamOIDCCustomSessionData(oidcUpstream provider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token) (*psession.CustomSessionData, error) {
|
||||||
|
upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, oidcUpstream.GetName(), token.IDToken.Claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
upstreamIssuer, err := ExtractStringClaimValue(oidc.IDTokenIssuerClaim, oidcUpstream.GetName(), token.IDToken.Claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
customSessionData := &psession.CustomSessionData{
|
||||||
|
ProviderUID: oidcUpstream.GetResourceUID(),
|
||||||
|
ProviderName: oidcUpstream.GetName(),
|
||||||
|
ProviderType: psession.ProviderTypeOIDC,
|
||||||
|
OIDC: &psession.OIDCSessionData{
|
||||||
|
UpstreamIssuer: upstreamIssuer,
|
||||||
|
UpstreamSubject: upstreamSubject,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const pleaseCheck = "please check configuration of OIDCIdentityProvider and the client in the " +
|
||||||
|
"upstream provider's API/UI and try to get a refresh token if possible"
|
||||||
|
logKV := []interface{}{
|
||||||
|
"upstreamName", oidcUpstream.GetName(),
|
||||||
|
"scopes", oidcUpstream.GetScopes(),
|
||||||
|
"additionalParams", oidcUpstream.GetAdditionalAuthcodeParams(),
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRefreshToken := token.RefreshToken != nil && token.RefreshToken.Token != ""
|
||||||
|
hasAccessToken := token.AccessToken != nil && token.AccessToken.Token != ""
|
||||||
|
switch {
|
||||||
|
case hasRefreshToken: // we prefer refresh tokens, so check for this first
|
||||||
|
customSessionData.OIDC.UpstreamRefreshToken = token.RefreshToken.Token
|
||||||
|
case hasAccessToken: // as a fallback, we can use the access token as long as there is a userinfo endpoint
|
||||||
|
if !oidcUpstream.HasUserInfoURL() {
|
||||||
|
plog.Warning("access token was returned by upstream provider during login without a refresh token "+
|
||||||
|
"and there was no userinfo endpoint available on the provider. "+pleaseCheck, logKV...)
|
||||||
|
return nil, errors.New("access token was returned by upstream provider but there was no userinfo endpoint")
|
||||||
|
}
|
||||||
|
plog.Info("refresh token not returned by upstream provider during login, using access token instead. "+pleaseCheck, logKV...)
|
||||||
|
customSessionData.OIDC.UpstreamAccessToken = token.AccessToken.Token
|
||||||
|
default:
|
||||||
|
plog.Warning("refresh token and access token not returned by upstream provider during login. "+pleaseCheck, logKV...)
|
||||||
|
return nil, errors.New("neither access token nor refresh token returned by upstream provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return customSessionData, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GrantScopesIfRequested auto-grants the scopes for which we do not require end-user approval, if they were requested.
|
// GrantScopesIfRequested auto-grants the scopes for which we do not require end-user approval, if they were requested.
|
||||||
func GrantScopesIfRequested(authorizeRequester fosite.AuthorizeRequester) {
|
func GrantScopesIfRequested(authorizeRequester fosite.AuthorizeRequester) {
|
||||||
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID)
|
||||||
@ -89,11 +140,11 @@ func getSubjectAndUsernameFromUpstreamIDToken(
|
|||||||
) (string, string, error) {
|
) (string, string, error) {
|
||||||
// The spec says the "sub" claim is only unique per issuer,
|
// The spec says the "sub" claim is only unique per issuer,
|
||||||
// so we will prepend the issuer string to make it globally unique.
|
// so we will prepend the issuer string to make it globally unique.
|
||||||
upstreamIssuer, err := extractStringClaimValue(oidc.IDTokenIssuerClaim, upstreamIDPConfig.GetName(), idTokenClaims)
|
upstreamIssuer, err := ExtractStringClaimValue(oidc.IDTokenIssuerClaim, upstreamIDPConfig.GetName(), idTokenClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
upstreamSubject, err := extractStringClaimValue(oidc.IDTokenSubjectClaim, upstreamIDPConfig.GetName(), idTokenClaims)
|
upstreamSubject, err := ExtractStringClaimValue(oidc.IDTokenSubjectClaim, upstreamIDPConfig.GetName(), idTokenClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@ -128,7 +179,7 @@ func getSubjectAndUsernameFromUpstreamIDToken(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
username, err := extractStringClaimValue(usernameClaimName, upstreamIDPConfig.GetName(), idTokenClaims)
|
username, err := ExtractStringClaimValue(usernameClaimName, upstreamIDPConfig.GetName(), idTokenClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@ -136,7 +187,7 @@ func getSubjectAndUsernameFromUpstreamIDToken(
|
|||||||
return subject, username, nil
|
return subject, username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenClaims map[string]interface{}) (string, error) {
|
func ExtractStringClaimValue(claimName string, upstreamIDPName string, idTokenClaims map[string]interface{}) (string, error) {
|
||||||
value, ok := idTokenClaims[claimName]
|
value, ok := idTokenClaims[claimName]
|
||||||
if !ok {
|
if !ok {
|
||||||
plog.Warning(
|
plog.Warning(
|
||||||
|
@ -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 oidc
|
package oidc
|
||||||
@ -114,7 +114,7 @@ func (k KubeStorage) GetOpenIDConnectSession(ctx context.Context, fullAuthcode s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k KubeStorage) DeleteOpenIDConnectSession(ctx context.Context, fullAuthcode string) error {
|
func (k KubeStorage) DeleteOpenIDConnectSession(ctx context.Context, fullAuthcode string) error {
|
||||||
return k.oidcStorage.DeleteOpenIDConnectSession(ctx, fullAuthcode)
|
return k.oidcStorage.DeleteOpenIDConnectSession(ctx, fullAuthcode) //nolint: staticcheck // we know this is deprecated and never called. our GC controller cleans these up.
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -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 provider
|
package provider
|
||||||
@ -40,6 +40,9 @@ type UpstreamOIDCIdentityProviderI interface {
|
|||||||
// GetAuthorizationURL returns the Authorization Endpoint fetched from discovery.
|
// GetAuthorizationURL returns the Authorization Endpoint fetched from discovery.
|
||||||
GetAuthorizationURL() *url.URL
|
GetAuthorizationURL() *url.URL
|
||||||
|
|
||||||
|
// HasUserInfoURL returns whether there is a non-empty value for userinfo_endpoint fetched from discovery.
|
||||||
|
HasUserInfoURL() bool
|
||||||
|
|
||||||
// GetScopes returns the scopes to request in authorization (authcode or password grant) flow.
|
// GetScopes returns the scopes to request in authorization (authcode or password grant) flow.
|
||||||
GetScopes() []string
|
GetScopes() []string
|
||||||
|
|
||||||
@ -83,10 +86,10 @@ type UpstreamOIDCIdentityProviderI interface {
|
|||||||
// represent an error such that it is not worth retrying revocation later, even though revocation failed.
|
// represent an error such that it is not worth retrying revocation later, even though revocation failed.
|
||||||
RevokeToken(ctx context.Context, token string, tokenType RevocableTokenType) error
|
RevokeToken(ctx context.Context, token string, tokenType RevocableTokenType) error
|
||||||
|
|
||||||
// ValidateToken will validate the ID token. It will also merge the claims from the userinfo endpoint response
|
// ValidateTokenAndMergeWithUserInfo will validate the ID token. It will also merge the claims from the userinfo endpoint response
|
||||||
// into the ID token's claims, if the provider offers the userinfo endpoint. It returns the validated/updated
|
// into the ID token's claims, if the provider offers the userinfo endpoint. It returns the validated/updated
|
||||||
// tokens, or an error.
|
// tokens, or an error.
|
||||||
ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error)
|
ValidateTokenAndMergeWithUserInfo(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, requireIDToken bool, requireUserInfo bool) (*oidctypes.Token, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpstreamLDAPIdentityProviderI interface {
|
type UpstreamLDAPIdentityProviderI interface {
|
||||||
@ -105,7 +108,14 @@ type UpstreamLDAPIdentityProviderI interface {
|
|||||||
authenticators.UserAuthenticator
|
authenticators.UserAuthenticator
|
||||||
|
|
||||||
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
||||||
PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error
|
PerformRefresh(ctx context.Context, storedRefreshAttributes StoredRefreshAttributes) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoredRefreshAttributes struct {
|
||||||
|
Username string
|
||||||
|
Subject string
|
||||||
|
DN string
|
||||||
|
AdditionalAttributes map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicUpstreamIDPProvider interface {
|
type DynamicUpstreamIDPProvider interface {
|
||||||
|
@ -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
|
||||||
@ -121,7 +121,7 @@ func TestManager(t *testing.T) {
|
|||||||
r.False(fallbackHandlerWasCalled)
|
r.False(fallbackHandlerWasCalled)
|
||||||
|
|
||||||
// Minimal check to ensure that the right endpoint was called
|
// Minimal check to ensure that the right endpoint was called
|
||||||
r.Equal(http.StatusFound, recorder.Code)
|
r.Equal(http.StatusSeeOther, recorder.Code)
|
||||||
actualLocation := recorder.Header().Get("Location")
|
actualLocation := recorder.Header().Get("Location")
|
||||||
r.True(
|
r.True(
|
||||||
strings.HasPrefix(actualLocation, expectedRedirectLocationPrefix),
|
strings.HasPrefix(actualLocation, expectedRedirectLocationPrefix),
|
||||||
@ -160,7 +160,7 @@ func TestManager(t *testing.T) {
|
|||||||
|
|
||||||
// Check just enough of the response to ensure that we wired up the callback endpoint correctly.
|
// Check just enough of the response to ensure that we wired up the callback endpoint correctly.
|
||||||
// The endpoint's own unit tests cover everything else.
|
// The endpoint's own unit tests cover everything else.
|
||||||
r.Equal(http.StatusFound, recorder.Code)
|
r.Equal(http.StatusSeeOther, recorder.Code)
|
||||||
actualLocation := recorder.Header().Get("Location")
|
actualLocation := recorder.Header().Get("Location")
|
||||||
r.True(
|
r.True(
|
||||||
strings.HasPrefix(actualLocation, downstreamRedirectURL),
|
strings.HasPrefix(actualLocation, downstreamRedirectURL),
|
||||||
|
@ -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 token provides a handler for the OIDC token endpoint.
|
// Package token provides a handler for the OIDC token endpoint.
|
||||||
@ -6,16 +6,19 @@ package token
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
"github.com/ory/x/errorsx"
|
"github.com/ory/x/errorsx"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"go.pinniped.dev/internal/httputil/httperr"
|
"go.pinniped.dev/internal/httputil/httperr"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"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"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -75,11 +78,6 @@ func NewHandler(
|
|||||||
|
|
||||||
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||||
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
||||||
downstreamUsername, err := getDownstreamUsernameFromPinnipedSession(session)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
downstreamSubject := session.Fosite.Claims.Subject
|
|
||||||
|
|
||||||
customSessionData := session.Custom
|
customSessionData := session.Custom
|
||||||
if customSessionData == nil {
|
if customSessionData == nil {
|
||||||
@ -93,18 +91,27 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
|
|||||||
|
|
||||||
switch customSessionData.ProviderType {
|
switch customSessionData.ProviderType {
|
||||||
case psession.ProviderTypeOIDC:
|
case psession.ProviderTypeOIDC:
|
||||||
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
|
return upstreamOIDCRefresh(ctx, session, providerCache)
|
||||||
case psession.ProviderTypeLDAP:
|
case psession.ProviderTypeLDAP:
|
||||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
return upstreamLDAPRefresh(ctx, providerCache, session)
|
||||||
case psession.ProviderTypeActiveDirectory:
|
case psession.ProviderTypeActiveDirectory:
|
||||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
return upstreamLDAPRefresh(ctx, providerCache, session)
|
||||||
default:
|
default:
|
||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
func upstreamOIDCRefresh(ctx context.Context, session *psession.PinnipedSession, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||||
if s.OIDC == nil || s.OIDC.UpstreamRefreshToken == "" {
|
s := session.Custom
|
||||||
|
if s.OIDC == nil {
|
||||||
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
|
}
|
||||||
|
|
||||||
|
accessTokenStored := s.OIDC.UpstreamAccessToken != ""
|
||||||
|
refreshTokenStored := s.OIDC.UpstreamRefreshToken != ""
|
||||||
|
|
||||||
|
exactlyOneTokenStored := (accessTokenStored || refreshTokenStored) && !(accessTokenStored && refreshTokenStored)
|
||||||
|
if !exactlyOneTokenStored {
|
||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,42 +123,98 @@ func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
|||||||
plog.Debug("attempting upstream refresh request",
|
plog.Debug("attempting upstream refresh request",
|
||||||
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
|
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
|
||||||
|
|
||||||
refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken)
|
var tokens *oauth2.Token
|
||||||
if err != nil {
|
if refreshTokenStored {
|
||||||
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
tokens, err = p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken)
|
||||||
"Upstream refresh failed.",
|
if err != nil {
|
||||||
).WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||||
|
"Upstream refresh failed.",
|
||||||
|
).WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokens = &oauth2.Token{AccessToken: s.OIDC.UpstreamAccessToken}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream refresh may or may not return a new ID token. From the spec:
|
// Upstream refresh may or may not return a new ID token. From the spec:
|
||||||
// "the response body is the Token Response of Section 3.1.3.3 except that it might not contain an id_token."
|
// "the response body is the Token Response of Section 3.1.3.3 except that it might not contain an id_token."
|
||||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
|
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
|
||||||
_, hasIDTok := refreshedTokens.Extra("id_token").(string)
|
_, hasIDTok := tokens.Extra("id_token").(string)
|
||||||
if hasIDTok {
|
|
||||||
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at
|
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at
|
||||||
// least some providers do not include one, so we skip the nonce validation here (but not other validations).
|
// least some providers do not include one, so we skip the nonce validation here (but not other validations).
|
||||||
_, err = p.ValidateToken(ctx, refreshedTokens, "")
|
validatedTokens, err := p.ValidateTokenAndMergeWithUserInfo(ctx, tokens, "", hasIDTok, accessTokenStored)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||||
"Upstream refresh returned an invalid ID token.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
"Upstream refresh returned an invalid ID token or UserInfo response.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
plog.Debug("upstream refresh request did not return a new ID token",
|
err = validateIdentityUnchangedSinceInitialLogin(validatedTokens, session, p.GetUsernameClaim())
|
||||||
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in
|
// Upstream refresh may or may not return a new refresh token. If we got a new refresh token, then update it in
|
||||||
// the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding
|
// the user's session. If we did not get a new refresh token, then keep the old one in the session by avoiding
|
||||||
// overwriting the old one.
|
// overwriting the old one.
|
||||||
if refreshedTokens.RefreshToken != "" {
|
if tokens.RefreshToken != "" {
|
||||||
plog.Debug("upstream refresh request did not return a new refresh token",
|
plog.Debug("upstream refresh request returned a new refresh token",
|
||||||
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
|
"providerName", s.ProviderName, "providerType", s.ProviderType, "providerUID", s.ProviderUID)
|
||||||
s.OIDC.UpstreamRefreshToken = refreshedTokens.RefreshToken
|
s.OIDC.UpstreamRefreshToken = tokens.RefreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateIdentityUnchangedSinceInitialLogin(validatedTokens *oidctypes.Token, session *psession.PinnipedSession, usernameClaimName string) error {
|
||||||
|
s := session.Custom
|
||||||
|
mergedClaims := validatedTokens.IDToken.Claims
|
||||||
|
|
||||||
|
// If we have any claims at all, we better have a subject, and it better match the previous value.
|
||||||
|
// but it's possible that we don't because both returning a new id token on refresh and having a userinfo
|
||||||
|
// endpoint are optional.
|
||||||
|
if len(mergedClaims) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newSub, hasSub := getString(mergedClaims, oidc.IDTokenSubjectClaim)
|
||||||
|
if !hasSub {
|
||||||
|
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||||
|
"Upstream refresh failed.").WithWrap(errors.New("subject in upstream refresh not found")).
|
||||||
|
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
if s.OIDC.UpstreamSubject != newSub {
|
||||||
|
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||||
|
"Upstream refresh failed.").WithWrap(errors.New("subject in upstream refresh does not match previous value")).
|
||||||
|
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
|
||||||
|
newUsername, hasUsername := getString(mergedClaims, usernameClaimName)
|
||||||
|
oldUsername := session.Fosite.Claims.Extra[oidc.DownstreamUsernameClaim]
|
||||||
|
// It's possible that a username wasn't returned by the upstream provider during refresh,
|
||||||
|
// but if it is, verify that it hasn't changed.
|
||||||
|
if hasUsername && oldUsername != newUsername {
|
||||||
|
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||||
|
"Upstream refresh failed.").WithWrap(errors.New("username in upstream refresh does not match previous value")).
|
||||||
|
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
|
||||||
|
newIssuer, hasIssuer := getString(mergedClaims, oidc.IDTokenIssuerClaim)
|
||||||
|
// It's possible that an issuer wasn't returned by the upstream provider during refresh,
|
||||||
|
// but if it is, verify that it hasn't changed.
|
||||||
|
if hasIssuer && s.OIDC.UpstreamIssuer != newIssuer {
|
||||||
|
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||||
|
"Upstream refresh failed.").WithWrap(errors.New("issuer in upstream refresh does not match previous value")).
|
||||||
|
WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getString(m map[string]interface{}, key string) (string, bool) {
|
||||||
|
val, ok := m[key].(string)
|
||||||
|
return val, ok
|
||||||
|
}
|
||||||
|
|
||||||
func findOIDCProviderByNameAndValidateUID(
|
func findOIDCProviderByNameAndValidateUID(
|
||||||
s *psession.CustomSessionData,
|
s *psession.CustomSessionData,
|
||||||
providerCache oidc.UpstreamIdentityProvidersLister,
|
providerCache oidc.UpstreamIdentityProvidersLister,
|
||||||
@ -169,7 +232,15 @@ func findOIDCProviderByNameAndValidateUID(
|
|||||||
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
}
|
}
|
||||||
|
|
||||||
func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister, username string, subject string) error {
|
func upstreamLDAPRefresh(ctx context.Context, providerCache oidc.UpstreamIdentityProvidersLister, session *psession.PinnipedSession) error {
|
||||||
|
username, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
subject := session.Fosite.Claims.Subject
|
||||||
|
|
||||||
|
s := session.Custom
|
||||||
|
|
||||||
// if you have neither a valid ldap session config nor a valid active directory session config
|
// if you have neither a valid ldap session config nor a valid active directory session config
|
||||||
validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != ""
|
validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != ""
|
||||||
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
|
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
|
||||||
@ -177,13 +248,28 @@ func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
|||||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var additionalAttributes map[string]string
|
||||||
|
if s.ProviderType == psession.ProviderTypeLDAP {
|
||||||
|
additionalAttributes = s.LDAP.ExtraRefreshAttributes
|
||||||
|
} else {
|
||||||
|
additionalAttributes = s.ActiveDirectory.ExtraRefreshAttributes
|
||||||
|
}
|
||||||
|
|
||||||
// get ldap/ad provider out of cache
|
// get ldap/ad provider out of cache
|
||||||
p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache)
|
p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if session.IDTokenClaims().AuthTime.IsZero() {
|
||||||
|
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||||
|
}
|
||||||
// run PerformRefresh
|
// run PerformRefresh
|
||||||
err = p.PerformRefresh(ctx, dn, username, subject)
|
err = p.PerformRefresh(ctx, provider.StoredRefreshAttributes{
|
||||||
|
Username: username,
|
||||||
|
Subject: subject,
|
||||||
|
DN: dn,
|
||||||
|
AdditionalAttributes: additionalAttributes,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||||
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||||
|
@ -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 token
|
package token
|
||||||
@ -52,10 +52,12 @@ import (
|
|||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
"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/pkg/oidcclient/oidctypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
goodIssuer = "https://some-issuer.com"
|
goodIssuer = "https://some-issuer.com"
|
||||||
|
goodUpstreamSubject = "some-subject"
|
||||||
goodClient = "pinniped-cli"
|
goodClient = "pinniped-cli"
|
||||||
goodRedirectURI = "http://127.0.0.1/callback"
|
goodRedirectURI = "http://127.0.0.1/callback"
|
||||||
goodPKCECodeVerifier = "some-pkce-verifier-that-must-be-at-least-43-characters-to-meet-entropy-requirements"
|
goodPKCECodeVerifier = "some-pkce-verifier-that-must-be-at-least-43-characters-to-meet-entropy-requirements"
|
||||||
@ -223,7 +225,7 @@ type expectedUpstreamRefresh struct {
|
|||||||
|
|
||||||
type expectedUpstreamValidateTokens struct {
|
type expectedUpstreamValidateTokens struct {
|
||||||
performedByUpstreamName string
|
performedByUpstreamName string
|
||||||
args *oidctestutil.ValidateTokenArgs
|
args *oidctestutil.ValidateTokenAndMergeWithUserInfoArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenEndpointResponseExpectedValues struct {
|
type tokenEndpointResponseExpectedValues struct {
|
||||||
@ -879,6 +881,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token"
|
oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token"
|
||||||
oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token"
|
oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token"
|
||||||
oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token"
|
oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token"
|
||||||
|
oidcUpstreamAccessToken = "fake-upstream-access-token" //nolint:gosec
|
||||||
|
|
||||||
ldapUpstreamName = "some-ldap-idp"
|
ldapUpstreamName = "some-ldap-idp"
|
||||||
ldapUpstreamResourceUID = "ldap-resource-uid"
|
ldapUpstreamResourceUID = "ldap-resource-uid"
|
||||||
@ -902,19 +905,34 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
WithResourceUID(oidcUpstreamResourceUID)
|
WithResourceUID(oidcUpstreamResourceUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
initialUpstreamOIDCCustomSessionData := func() *psession.CustomSessionData {
|
initialUpstreamOIDCRefreshTokenCustomSessionData := func() *psession.CustomSessionData {
|
||||||
return &psession.CustomSessionData{
|
return &psession.CustomSessionData{
|
||||||
ProviderName: oidcUpstreamName,
|
ProviderName: oidcUpstreamName,
|
||||||
ProviderUID: oidcUpstreamResourceUID,
|
ProviderUID: oidcUpstreamResourceUID,
|
||||||
ProviderType: oidcUpstreamType,
|
ProviderType: oidcUpstreamType,
|
||||||
OIDC: &psession.OIDCSessionData{
|
OIDC: &psession.OIDCSessionData{
|
||||||
UpstreamRefreshToken: oidcUpstreamInitialRefreshToken,
|
UpstreamRefreshToken: oidcUpstreamInitialRefreshToken,
|
||||||
|
UpstreamSubject: goodUpstreamSubject,
|
||||||
|
UpstreamIssuer: goodIssuer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialUpstreamOIDCAccessTokenCustomSessionData := func() *psession.CustomSessionData {
|
||||||
|
return &psession.CustomSessionData{
|
||||||
|
ProviderName: oidcUpstreamName,
|
||||||
|
ProviderUID: oidcUpstreamResourceUID,
|
||||||
|
ProviderType: oidcUpstreamType,
|
||||||
|
OIDC: &psession.OIDCSessionData{
|
||||||
|
UpstreamAccessToken: oidcUpstreamAccessToken,
|
||||||
|
UpstreamSubject: goodUpstreamSubject,
|
||||||
|
UpstreamIssuer: goodIssuer,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
upstreamOIDCCustomSessionDataWithNewRefreshToken := func(newRefreshToken string) *psession.CustomSessionData {
|
upstreamOIDCCustomSessionDataWithNewRefreshToken := func(newRefreshToken string) *psession.CustomSessionData {
|
||||||
sessionData := initialUpstreamOIDCCustomSessionData()
|
sessionData := initialUpstreamOIDCRefreshTokenCustomSessionData()
|
||||||
sessionData.OIDC.UpstreamRefreshToken = newRefreshToken
|
sessionData.OIDC.UpstreamRefreshToken = newRefreshToken
|
||||||
return sessionData
|
return sessionData
|
||||||
}
|
}
|
||||||
@ -953,13 +971,15 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token) *expectedUpstreamValidateTokens {
|
happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token, requireIDToken bool) *expectedUpstreamValidateTokens {
|
||||||
return &expectedUpstreamValidateTokens{
|
return &expectedUpstreamValidateTokens{
|
||||||
performedByUpstreamName: oidcUpstreamName,
|
performedByUpstreamName: oidcUpstreamName,
|
||||||
args: &oidctestutil.ValidateTokenArgs{
|
args: &oidctestutil.ValidateTokenAndMergeWithUserInfoArgs{
|
||||||
Ctx: nil, // this will be filled in with the actual request context by the test below
|
Ctx: nil, // this will be filled in with the actual request context by the test below
|
||||||
Tok: expectedTokens,
|
Tok: expectedTokens,
|
||||||
ExpectedIDTokenNonce: "", // always expect empty string
|
ExpectedIDTokenNonce: "", // always expect empty string
|
||||||
|
RequireUserInfo: false,
|
||||||
|
RequireIDToken: requireIDToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -981,9 +1001,8 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
||||||
// Should always try to perform an upstream refresh.
|
// Should always try to perform an upstream refresh.
|
||||||
want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall()
|
want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall()
|
||||||
// Should only try to ValidateToken when there was an id token returned by the upstream refresh.
|
|
||||||
if expectToValidateToken != nil {
|
if expectToValidateToken != nil {
|
||||||
want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken)
|
want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken, true)
|
||||||
}
|
}
|
||||||
return want
|
return want
|
||||||
}
|
}
|
||||||
@ -1046,11 +1065,17 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "happy path refresh grant with openid scope granted (id token returned)",
|
name: "happy path refresh grant with openid scope granted (id token returned)",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()),
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
want: happyRefreshTokenResponseForOpenIDAndOfflineAccess(
|
want: happyRefreshTokenResponseForOpenIDAndOfflineAccess(
|
||||||
@ -1059,19 +1084,88 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "refresh grant with unchanged username claim",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
|
upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"some-claim": "some-value",
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
"username-claim": goodUsername,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: happyRefreshTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||||
|
refreshedUpstreamTokensWithIDAndRefreshTokens(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "refresh grant when the customsessiondata has a stored access token and no stored refresh token",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
|
upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").
|
||||||
|
WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"some-claim": "some-value",
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
"username-claim": goodUsername,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: oidcUpstreamAccessToken,
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
customSessionData: initialUpstreamOIDCAccessTokenCustomSessionData(),
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCAccessTokenCustomSessionData()),
|
||||||
|
}, // do not want upstreamRefreshRequest???
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantSuccessBodyFields: []string{"refresh_token", "id_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
|
wantRequestedScopes: []string{"openid", "offline_access"},
|
||||||
|
wantGrantedScopes: []string{"openid", "offline_access"},
|
||||||
|
wantUpstreamOIDCValidateTokenCall: &expectedUpstreamValidateTokens{
|
||||||
|
oidcUpstreamName,
|
||||||
|
&oidctestutil.ValidateTokenAndMergeWithUserInfoArgs{
|
||||||
|
Ctx: nil, // this will be filled in with the actual request context by the test below
|
||||||
|
Tok: &oauth2.Token{AccessToken: oidcUpstreamAccessToken}, // only the old access token
|
||||||
|
ExpectedIDTokenNonce: "", // always expect empty string
|
||||||
|
RequireIDToken: false,
|
||||||
|
RequireUserInfo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantCustomSessionDataStored: initialUpstreamOIDCAccessTokenCustomSessionData(), // doesn't change when we refresh
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "happy path refresh grant without openid scope granted (no id token returned)",
|
name: "happy path refresh grant without openid scope granted (no id token returned)",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken()).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
wantRequestedScopes: []string{"offline_access"},
|
wantRequestedScopes: []string{"offline_access"},
|
||||||
wantGrantedScopes: []string{"offline_access"},
|
wantGrantedScopes: []string{"offline_access"},
|
||||||
wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(),
|
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
@ -1081,7 +1175,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
wantRequestedScopes: []string{"offline_access"},
|
wantRequestedScopes: []string{"offline_access"},
|
||||||
wantGrantedScopes: []string{"offline_access"},
|
wantGrantedScopes: []string{"offline_access"},
|
||||||
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false),
|
||||||
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1089,31 +1183,46 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "happy path refresh grant when the upstream refresh does not return a new ID token",
|
name: "happy path refresh grant when the upstream refresh does not return a new ID token",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken()).Build()),
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken()).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()),
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
want: happyRefreshTokenResponseForOpenIDAndOfflineAccess(
|
want: tokenEndpointResponseExpectedValues{
|
||||||
upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
wantStatus: http.StatusOK,
|
||||||
nil, // expect ValidateToken is *not* called
|
wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"},
|
||||||
),
|
wantRequestedScopes: []string{"openid", "offline_access"},
|
||||||
|
wantGrantedScopes: []string{"openid", "offline_access"},
|
||||||
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false),
|
||||||
|
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path refresh grant when the upstream refresh does not return a new refresh token",
|
name: "happy path refresh grant when the upstream refresh does not return a new refresh token",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDTokenWithoutRefreshToken()).Build()),
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDTokenWithoutRefreshToken()).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()),
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
want: happyRefreshTokenResponseForOpenIDAndOfflineAccess(
|
want: happyRefreshTokenResponseForOpenIDAndOfflineAccess(
|
||||||
initialUpstreamOIDCCustomSessionData(), // still has the initial refresh token stored
|
initialUpstreamOIDCRefreshTokenCustomSessionData(), // still has the initial refresh token stored
|
||||||
refreshedUpstreamTokensWithIDTokenWithoutRefreshToken(),
|
refreshedUpstreamTokensWithIDTokenWithoutRefreshToken(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -1121,11 +1230,17 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "when the refresh request adds a new scope to the list of requested scopes then it is ignored",
|
name: "when the refresh request adds a new scope to the list of requested scopes then it is ignored",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()),
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
|
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
|
||||||
@ -1140,16 +1255,22 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "when the refresh request removes a scope which was originally granted from the list of requested scopes then it is granted anyway",
|
name: "when the refresh request removes a scope which was originally granted from the list of requested scopes then it is granted anyway",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access pinniped:request-audience") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access pinniped:request-audience") },
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(),
|
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
@ -1162,7 +1283,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||||
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
|
||||||
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1170,11 +1291,17 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "when the refresh request does not include a scope param then it gets all the same scopes as the original authorization request",
|
name: "when the refresh request does not include a scope param then it gets all the same scopes as the original authorization request",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
upstreamOIDCIdentityProviderBuilder().WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()),
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
|
modifyTokenRequest: func(r *http.Request, refreshToken string, accessToken string) {
|
||||||
@ -1190,14 +1317,14 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
name: "when a bad refresh token is sent in the refresh request",
|
name: "when a bad refresh token is sent in the refresh request",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
wantRequestedScopes: []string{"offline_access"},
|
wantRequestedScopes: []string{"offline_access"},
|
||||||
wantGrantedScopes: []string{"offline_access"},
|
wantGrantedScopes: []string{"offline_access"},
|
||||||
wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(),
|
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
@ -1214,14 +1341,14 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
name: "when the access token is sent as if it were a refresh token",
|
name: "when the access token is sent as if it were a refresh token",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
wantRequestedScopes: []string{"offline_access"},
|
wantRequestedScopes: []string{"offline_access"},
|
||||||
wantGrantedScopes: []string{"offline_access"},
|
wantGrantedScopes: []string{"offline_access"},
|
||||||
wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(),
|
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
@ -1238,14 +1365,14 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
name: "when the wrong client ID is included in the refresh request",
|
name: "when the wrong client ID is included in the refresh request",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access") },
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||||
wantRequestedScopes: []string{"offline_access"},
|
wantRequestedScopes: []string{"offline_access"},
|
||||||
wantGrantedScopes: []string{"offline_access"},
|
wantGrantedScopes: []string{"offline_access"},
|
||||||
wantCustomSessionDataStored: initialUpstreamOIDCCustomSessionData(),
|
wantCustomSessionDataStored: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
@ -1409,7 +1536,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "when there is no OIDC refresh token in custom session data found in the session storage during the refresh request",
|
name: "when there is no OIDC refresh token nor access token in custom session data found in the session storage during the refresh request",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: &psession.CustomSessionData{
|
customSessionData: &psession.CustomSessionData{
|
||||||
@ -1417,7 +1544,8 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
ProviderUID: oidcUpstreamResourceUID,
|
ProviderUID: oidcUpstreamResourceUID,
|
||||||
ProviderType: oidcUpstreamType,
|
ProviderType: oidcUpstreamType,
|
||||||
OIDC: &psession.OIDCSessionData{
|
OIDC: &psession.OIDCSessionData{
|
||||||
UpstreamRefreshToken: "", // this should not happen in practice
|
UpstreamRefreshToken: "", // this should not happen in practice. we should always have exactly one of these.
|
||||||
|
UpstreamAccessToken: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
@ -1428,6 +1556,7 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
ProviderType: oidcUpstreamType,
|
ProviderType: oidcUpstreamType,
|
||||||
OIDC: &psession.OIDCSessionData{
|
OIDC: &psession.OIDCSessionData{
|
||||||
UpstreamRefreshToken: "", // this should not happen in practice
|
UpstreamRefreshToken: "", // this should not happen in practice
|
||||||
|
UpstreamAccessToken: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -1508,9 +1637,9 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().
|
||||||
WithPerformRefreshError(errors.New("some upstream refresh error")).Build()),
|
WithPerformRefreshError(errors.New("some upstream refresh error")).Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()),
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
@ -1529,23 +1658,146 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
name: "when the upstream refresh returns an invalid ID token during the refresh request",
|
name: "when the upstream refresh returns an invalid ID token during the refresh request",
|
||||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().
|
||||||
WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).
|
WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).
|
||||||
// This is the current format of the errors returned by the production code version of ValidateToken, see ValidateToken in upstreamoidc.go
|
// This is the current format of the errors returned by the production code version of ValidateTokenAndMergeWithUserInfo, see ValidateTokenAndMergeWithUserInfo in upstreamoidc.go
|
||||||
WithValidateTokenError(httperr.Wrap(http.StatusBadRequest, "some validate error", errors.New("some validate cause"))).
|
WithValidateTokenAndMergeWithUserInfoError(httperr.Wrap(http.StatusBadRequest, "some validate error", errors.New("some validate cause"))).
|
||||||
Build()),
|
Build()),
|
||||||
authcodeExchange: authcodeExchangeInputs{
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
customSessionData: initialUpstreamOIDCCustomSessionData(),
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCCustomSessionData()),
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
},
|
},
|
||||||
refreshRequest: refreshRequestInputs{
|
refreshRequest: refreshRequestInputs{
|
||||||
want: tokenEndpointResponseExpectedValues{
|
want: tokenEndpointResponseExpectedValues{
|
||||||
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
|
||||||
wantStatus: http.StatusUnauthorized,
|
wantStatus: http.StatusUnauthorized,
|
||||||
wantErrorResponseBody: here.Doc(`
|
wantErrorResponseBody: here.Doc(`
|
||||||
{
|
{
|
||||||
"error": "error",
|
"error": "error",
|
||||||
"error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token."
|
"error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token or UserInfo response."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the upstream refresh returns an ID token with a different subject than the original",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().
|
||||||
|
WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).
|
||||||
|
// This is the current format of the errors returned by the production code version of ValidateTokenAndMergeWithUserInfo, see ValidateTokenAndMergeWithUserInfo in upstreamoidc.go
|
||||||
|
WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"sub": "something-different",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
Build()),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "refresh grant with claims but not the subject claim",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
|
upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"some-claim": "some-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "refresh grant with changed username claim",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
|
upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"some-claim": "some-value",
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
"username-claim": "some-changed-username",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "refresh grant with changed issuer claim",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(
|
||||||
|
upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"some-claim": "some-value",
|
||||||
|
"sub": goodUpstreamSubject,
|
||||||
|
"iss": "some-changed-issuer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()).Build()),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(),
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamOIDCRefreshTokenCustomSessionData()),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||||
|
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true),
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
},
|
},
|
||||||
@ -2058,6 +2310,219 @@ func TestRefreshGrant(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "upstream ldap idp not found",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "upstream active directory idp not found",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fosite session is empty",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username not found in extra field",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
session.Fosite = &openid.DefaultSession{
|
||||||
|
Claims: &jwt.IDTokenClaims{
|
||||||
|
Extra: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auth time is the zero value", // time.Times can never be nil, but it is possible that it would be the zero value which would mean something's wrong
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: ldapUpstreamResourceUID,
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
|
||||||
|
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
|
||||||
|
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
session := firstRequester.GetSession().(*psession.PinnipedSession)
|
||||||
|
fositeSessionClaims := session.Fosite.IDTokenClaims()
|
||||||
|
fositeSessionClaims.AuthTime = time.Time{}
|
||||||
|
session.Fosite.Claims = fositeSessionClaims
|
||||||
|
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusInternalServerError,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "There was an internal server error. Required upstream data not found in session."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the ldap provider in the session storage is found but has the wrong resource UID during the refresh request",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: ldapUpstreamName,
|
||||||
|
ResourceUID: "the-wrong-uid",
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyLDAPCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyLDAPCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "when the active directory provider in the session storage is found but has the wrong resource UID during the refresh request",
|
||||||
|
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||||
|
Name: activeDirectoryUpstreamName,
|
||||||
|
ResourceUID: "the-wrong-uid",
|
||||||
|
URL: ldapUpstreamURL,
|
||||||
|
}),
|
||||||
|
authcodeExchange: authcodeExchangeInputs{
|
||||||
|
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||||
|
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||||
|
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||||
|
happyActiveDirectoryCustomSessionData,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refreshRequest: refreshRequestInputs{
|
||||||
|
want: tokenEndpointResponseExpectedValues{
|
||||||
|
wantStatus: http.StatusUnauthorized,
|
||||||
|
wantErrorResponseBody: here.Doc(`
|
||||||
|
{
|
||||||
|
"error": "error",
|
||||||
|
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
test := test
|
test := test
|
||||||
@ -2758,7 +3223,7 @@ func requireValidStoredRequest(
|
|||||||
|
|
||||||
// At this time, we don't use any of these optional (per the OIDC spec) fields.
|
// At this time, we don't use any of these optional (per the OIDC spec) fields.
|
||||||
require.Empty(t, claims.AuthenticationContextClassReference)
|
require.Empty(t, claims.AuthenticationContextClassReference)
|
||||||
require.Empty(t, claims.AuthenticationMethodsReference)
|
require.Empty(t, claims.AuthenticationMethodsReferences)
|
||||||
require.Empty(t, claims.CodeHash)
|
require.Empty(t, claims.CodeHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,44 +1,14 @@
|
|||||||
// 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 plog
|
package plog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint: gochecknoglobals
|
|
||||||
var removeKlogGlobalFlagsLock sync.Mutex
|
|
||||||
|
|
||||||
// RemoveKlogGlobalFlags attempts to "remove" flags that get unconditionally added by importing klog.
|
|
||||||
func RemoveKlogGlobalFlags() {
|
|
||||||
// since we mess with global state, we need a lock to synchronize us when called in parallel during tests
|
|
||||||
removeKlogGlobalFlagsLock.Lock()
|
|
||||||
defer removeKlogGlobalFlagsLock.Unlock()
|
|
||||||
|
|
||||||
// if this function starts to panic, it likely means that klog stopped mucking with global flags
|
|
||||||
const globalLogFlushFlag = "log-flush-frequency"
|
|
||||||
if err := pflag.CommandLine.MarkHidden(globalLogFlushFlag); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := pflag.CommandLine.MarkDeprecated(globalLogFlushFlag, "unsupported"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if pflag.CommandLine.Changed(globalLogFlushFlag) {
|
|
||||||
panic("unsupported global klog flag set")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KRef is (mostly) copied from klog - it is a standard way to represent a metav1.Object in logs
|
|
||||||
// when you only have access to the namespace and name of the object.
|
|
||||||
func KRef(namespace, name string) string {
|
|
||||||
return fmt.Sprintf("%s/%s", namespace, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KObj is (mostly) copied from klog - it is a standard way to represent a metav1.Object in logs.
|
// KObj is (mostly) copied from klog - it is a standard way to represent a metav1.Object in logs.
|
||||||
func KObj(obj klog.KMetadata) string {
|
func KObj(obj klog.KMetadata) string {
|
||||||
return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
|
return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
|
||||||
|
@ -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 psession
|
package psession
|
||||||
@ -74,16 +74,26 @@ type OIDCSessionData struct {
|
|||||||
// non-empty, then this field should be empty, indicating that we should use the upstream refresh token during
|
// non-empty, then this field should be empty, indicating that we should use the upstream refresh token during
|
||||||
// downstream refresh.
|
// downstream refresh.
|
||||||
UpstreamAccessToken string `json:"upstreamAccessToken"`
|
UpstreamAccessToken string `json:"upstreamAccessToken"`
|
||||||
|
|
||||||
|
// UpstreamSubject is the "sub" claim from the upstream identity provider from the user's initial login. We store this
|
||||||
|
// so that we can validate that it does not change upon refresh.
|
||||||
|
UpstreamSubject string `json:"upstreamSubject"`
|
||||||
|
|
||||||
|
// UpstreamIssuer is the "iss" claim from the upstream identity provider from the user's initial login. We store this
|
||||||
|
// so that we can validate that it does not change upon refresh.
|
||||||
|
UpstreamIssuer string `json:"upstreamIssuer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
|
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
|
||||||
type LDAPSessionData struct {
|
type LDAPSessionData struct {
|
||||||
UserDN string `json:"userDN"`
|
UserDN string `json:"userDN"`
|
||||||
|
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
|
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
|
||||||
type ActiveDirectorySessionData struct {
|
type ActiveDirectorySessionData struct {
|
||||||
UserDN string `json:"userDN"`
|
UserDN string `json:"userDN"`
|
||||||
|
ExtraRefreshAttributes map[string]string `json:"extraRefreshAttributes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPinnipedSession returns a new empty session.
|
// NewPinnipedSession returns a new empty session.
|
||||||
|
@ -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 credentialrequest
|
package credentialrequest
|
||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/sclevine/spec"
|
"github.com/sclevine/spec"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -71,11 +72,11 @@ func TestCreate(t *testing.T) {
|
|||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
ctrl = gomock.NewController(t)
|
ctrl = gomock.NewController(t)
|
||||||
logger = testutil.NewTranscriptLogger(t)
|
logger = testutil.NewTranscriptLogger(t)
|
||||||
klog.SetLogger(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 :(
|
||||||
})
|
})
|
||||||
|
|
||||||
it.After(func() {
|
it.After(func() {
|
||||||
klog.SetLogger(nil)
|
klog.ClearLogger()
|
||||||
ctrl.Finish()
|
ctrl.Finish()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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 server defines the entrypoint for the Pinniped Supervisor server.
|
// Package server defines the entrypoint for the Pinniped Supervisor server.
|
||||||
@ -20,7 +20,6 @@ import (
|
|||||||
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/clock"
|
|
||||||
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"
|
||||||
@ -29,6 +28,7 @@ import (
|
|||||||
"k8s.io/component-base/logs"
|
"k8s.io/component-base/logs"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
"k8s.io/klog/v2/klogr"
|
"k8s.io/klog/v2/klogr"
|
||||||
|
"k8s.io/utils/clock"
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||||
@ -431,7 +431,6 @@ func runSupervisor(podInfo *downward.PodInfo, cfg *supervisor.Config) error {
|
|||||||
func main() error { // return an error instead of klog.Fatal to allow defer statements to run
|
func main() error { // return an error instead of klog.Fatal to allow defer statements to run
|
||||||
logs.InitLogs()
|
logs.InitLogs()
|
||||||
defer logs.FlushLogs()
|
defer logs.FlushLogs()
|
||||||
plog.RemoveKlogGlobalFlags() // move this whenever the below code gets refactored to use cobra
|
|
||||||
|
|
||||||
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
|
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
|
||||||
klog.Infof("Command-line arguments were: %s %s %s", os.Args[0], os.Args[1], os.Args[2])
|
klog.Infof("Command-line arguments were: %s %s %s", os.Args[0], os.Args[1], os.Args[2])
|
||||||
|
@ -1,90 +1,13 @@
|
|||||||
// 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 testutil
|
package testutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
appsv1client "k8s.io/client-go/kubernetes/typed/apps/v1"
|
|
||||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDeleteOptionsRecorder(client kubernetes.Interface, opts *[]metav1.DeleteOptions) kubernetes.Interface {
|
|
||||||
return &clientWrapper{
|
|
||||||
Interface: client,
|
|
||||||
opts: opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type clientWrapper struct {
|
|
||||||
kubernetes.Interface
|
|
||||||
opts *[]metav1.DeleteOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientWrapper) CoreV1() corev1client.CoreV1Interface {
|
|
||||||
return &coreWrapper{CoreV1Interface: c.Interface.CoreV1(), opts: c.opts}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *clientWrapper) AppsV1() appsv1client.AppsV1Interface {
|
|
||||||
return &appsWrapper{AppsV1Interface: c.Interface.AppsV1(), opts: c.opts}
|
|
||||||
}
|
|
||||||
|
|
||||||
type coreWrapper struct {
|
|
||||||
corev1client.CoreV1Interface
|
|
||||||
opts *[]metav1.DeleteOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *coreWrapper) Pods(namespace string) corev1client.PodInterface {
|
|
||||||
return &podsWrapper{PodInterface: c.CoreV1Interface.Pods(namespace), opts: c.opts}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *coreWrapper) Secrets(namespace string) corev1client.SecretInterface {
|
|
||||||
return &secretsWrapper{SecretInterface: c.CoreV1Interface.Secrets(namespace), opts: c.opts}
|
|
||||||
}
|
|
||||||
|
|
||||||
type appsWrapper struct {
|
|
||||||
appsv1client.AppsV1Interface
|
|
||||||
opts *[]metav1.DeleteOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *appsWrapper) Deployments(namespace string) appsv1client.DeploymentInterface {
|
|
||||||
return &deploymentsWrapper{DeploymentInterface: c.AppsV1Interface.Deployments(namespace), opts: c.opts}
|
|
||||||
}
|
|
||||||
|
|
||||||
type podsWrapper struct {
|
|
||||||
corev1client.PodInterface
|
|
||||||
opts *[]metav1.DeleteOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *podsWrapper) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
|
|
||||||
*s.opts = append(*s.opts, opts)
|
|
||||||
return s.PodInterface.Delete(ctx, name, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
type secretsWrapper struct {
|
|
||||||
corev1client.SecretInterface
|
|
||||||
opts *[]metav1.DeleteOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *secretsWrapper) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
|
|
||||||
*s.opts = append(*s.opts, opts)
|
|
||||||
return s.SecretInterface.Delete(ctx, name, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
type deploymentsWrapper struct {
|
|
||||||
appsv1client.DeploymentInterface
|
|
||||||
opts *[]metav1.DeleteOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *deploymentsWrapper) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
|
|
||||||
*s.opts = append(*s.opts, opts)
|
|
||||||
return s.DeploymentInterface.Delete(ctx, name, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPreconditions(uid types.UID, rv string) metav1.DeleteOptions {
|
func NewPreconditions(uid types.UID, rv string) metav1.DeleteOptions {
|
||||||
return metav1.DeleteOptions{
|
return metav1.DeleteOptions{
|
||||||
Preconditions: &metav1.Preconditions{
|
Preconditions: &metav1.Preconditions{
|
||||||
|
@ -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 oidctestutil
|
package oidctestutil
|
||||||
@ -76,12 +76,20 @@ type RevokeTokenArgs struct {
|
|||||||
TokenType provider.RevocableTokenType
|
TokenType provider.RevocableTokenType
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateTokenArgs is used to spy on calls to
|
// ValidateTokenAndMergeWithUserInfoArgs is used to spy on calls to
|
||||||
// TestUpstreamOIDCIdentityProvider.ValidateTokenFunc().
|
// TestUpstreamOIDCIdentityProvider.ValidateTokenAndMergeWithUserInfoFunc().
|
||||||
type ValidateTokenArgs struct {
|
type ValidateTokenAndMergeWithUserInfoArgs struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Tok *oauth2.Token
|
Tok *oauth2.Token
|
||||||
ExpectedIDTokenNonce nonce.Nonce
|
ExpectedIDTokenNonce nonce.Nonce
|
||||||
|
RequireIDToken bool
|
||||||
|
RequireUserInfo bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidateRefreshArgs struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Tok *oauth2.Token
|
||||||
|
StoredAttributes provider.StoredRefreshAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestUpstreamLDAPIdentityProvider struct {
|
type TestUpstreamLDAPIdentityProvider struct {
|
||||||
@ -112,16 +120,16 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
|
|||||||
return u.URL
|
return u.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
|
||||||
if u.performRefreshArgs == nil {
|
if u.performRefreshArgs == nil {
|
||||||
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
||||||
}
|
}
|
||||||
u.performRefreshCallCount++
|
u.performRefreshCallCount++
|
||||||
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
|
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
DN: userDN,
|
DN: storedRefreshAttributes.DN,
|
||||||
ExpectedUsername: expectedUsername,
|
ExpectedUsername: storedRefreshAttributes.Username,
|
||||||
ExpectedSubject: expectedSubject,
|
ExpectedSubject: storedRefreshAttributes.Subject,
|
||||||
})
|
})
|
||||||
if u.PerformRefreshErr != nil {
|
if u.PerformRefreshErr != nil {
|
||||||
return u.PerformRefreshErr
|
return u.PerformRefreshErr
|
||||||
@ -145,6 +153,7 @@ type TestUpstreamOIDCIdentityProvider struct {
|
|||||||
ClientID string
|
ClientID string
|
||||||
ResourceUID types.UID
|
ResourceUID types.UID
|
||||||
AuthorizationURL url.URL
|
AuthorizationURL url.URL
|
||||||
|
UserInfoURL bool
|
||||||
RevocationURL *url.URL
|
RevocationURL *url.URL
|
||||||
UsernameClaim string
|
UsernameClaim string
|
||||||
GroupsClaim string
|
GroupsClaim string
|
||||||
@ -169,7 +178,7 @@ type TestUpstreamOIDCIdentityProvider struct {
|
|||||||
|
|
||||||
RevokeTokenFunc func(ctx context.Context, refreshToken string, tokenType provider.RevocableTokenType) error
|
RevokeTokenFunc func(ctx context.Context, refreshToken string, tokenType provider.RevocableTokenType) error
|
||||||
|
|
||||||
ValidateTokenFunc func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error)
|
ValidateTokenAndMergeWithUserInfoFunc func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error)
|
||||||
|
|
||||||
exchangeAuthcodeAndValidateTokensCallCount int
|
exchangeAuthcodeAndValidateTokensCallCount int
|
||||||
exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs
|
exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs
|
||||||
@ -179,8 +188,8 @@ type TestUpstreamOIDCIdentityProvider struct {
|
|||||||
performRefreshArgs []*PerformRefreshArgs
|
performRefreshArgs []*PerformRefreshArgs
|
||||||
revokeTokenCallCount int
|
revokeTokenCallCount int
|
||||||
revokeTokenArgs []*RevokeTokenArgs
|
revokeTokenArgs []*RevokeTokenArgs
|
||||||
validateTokenCallCount int
|
validateTokenAndMergeWithUserInfoCallCount int
|
||||||
validateTokenArgs []*ValidateTokenArgs
|
validateTokenAndMergeWithUserInfoArgs []*ValidateTokenAndMergeWithUserInfoArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ provider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{}
|
var _ provider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{}
|
||||||
@ -205,6 +214,10 @@ func (u *TestUpstreamOIDCIdentityProvider) GetAuthorizationURL() *url.URL {
|
|||||||
return &u.AuthorizationURL
|
return &u.AuthorizationURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamOIDCIdentityProvider) HasUserInfoURL() bool {
|
||||||
|
return u.UserInfoURL
|
||||||
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProvider) GetRevocationURL() *url.URL {
|
func (u *TestUpstreamOIDCIdentityProvider) GetRevocationURL() *url.URL {
|
||||||
return u.RevocationURL
|
return u.RevocationURL
|
||||||
}
|
}
|
||||||
@ -314,28 +327,30 @@ func (u *TestUpstreamOIDCIdentityProvider) RevokeTokenArgs(call int) *RevokeToke
|
|||||||
return u.revokeTokenArgs[call]
|
return u.revokeTokenArgs[call]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
|
func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfo(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, requireIDToken bool, requireUserInfo bool) (*oidctypes.Token, error) {
|
||||||
if u.validateTokenArgs == nil {
|
if u.validateTokenAndMergeWithUserInfoArgs == nil {
|
||||||
u.validateTokenArgs = make([]*ValidateTokenArgs, 0)
|
u.validateTokenAndMergeWithUserInfoArgs = make([]*ValidateTokenAndMergeWithUserInfoArgs, 0)
|
||||||
}
|
}
|
||||||
u.validateTokenCallCount++
|
u.validateTokenAndMergeWithUserInfoCallCount++
|
||||||
u.validateTokenArgs = append(u.validateTokenArgs, &ValidateTokenArgs{
|
u.validateTokenAndMergeWithUserInfoArgs = append(u.validateTokenAndMergeWithUserInfoArgs, &ValidateTokenAndMergeWithUserInfoArgs{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Tok: tok,
|
Tok: tok,
|
||||||
ExpectedIDTokenNonce: expectedIDTokenNonce,
|
ExpectedIDTokenNonce: expectedIDTokenNonce,
|
||||||
|
RequireIDToken: requireIDToken,
|
||||||
|
RequireUserInfo: requireUserInfo,
|
||||||
})
|
})
|
||||||
return u.ValidateTokenFunc(ctx, tok, expectedIDTokenNonce)
|
return u.ValidateTokenAndMergeWithUserInfoFunc(ctx, tok, expectedIDTokenNonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenCallCount() int {
|
func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfoCallCount() int {
|
||||||
return u.validateTokenCallCount
|
return u.validateTokenAndMergeWithUserInfoCallCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenArgs(call int) *ValidateTokenArgs {
|
func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfoArgs(call int) *ValidateTokenAndMergeWithUserInfoArgs {
|
||||||
if u.validateTokenArgs == nil {
|
if u.validateTokenAndMergeWithUserInfoArgs == nil {
|
||||||
u.validateTokenArgs = make([]*ValidateTokenArgs, 0)
|
u.validateTokenAndMergeWithUserInfoArgs = make([]*ValidateTokenAndMergeWithUserInfoArgs, 0)
|
||||||
}
|
}
|
||||||
return u.validateTokenArgs[call]
|
return u.validateTokenAndMergeWithUserInfoArgs[call]
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpstreamIDPListerBuilder struct {
|
type UpstreamIDPListerBuilder struct {
|
||||||
@ -520,25 +535,25 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *te
|
|||||||
func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToValidateToken(
|
func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToValidateToken(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
expectedPerformedByUpstreamName string,
|
expectedPerformedByUpstreamName string,
|
||||||
expectedArgs *ValidateTokenArgs,
|
expectedArgs *ValidateTokenAndMergeWithUserInfoArgs,
|
||||||
) {
|
) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var actualArgs *ValidateTokenArgs
|
var actualArgs *ValidateTokenAndMergeWithUserInfoArgs
|
||||||
var actualNameOfUpstreamWhichMadeCall string
|
var actualNameOfUpstreamWhichMadeCall string
|
||||||
actualCallCountAcrossAllOIDCUpstreams := 0
|
actualCallCountAcrossAllOIDCUpstreams := 0
|
||||||
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
||||||
callCountOnThisUpstream := upstreamOIDC.validateTokenCallCount
|
callCountOnThisUpstream := upstreamOIDC.validateTokenAndMergeWithUserInfoCallCount
|
||||||
actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream
|
actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream
|
||||||
if callCountOnThisUpstream == 1 {
|
if callCountOnThisUpstream == 1 {
|
||||||
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
|
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
|
||||||
actualArgs = upstreamOIDC.validateTokenArgs[0]
|
actualArgs = upstreamOIDC.validateTokenAndMergeWithUserInfoArgs[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams,
|
require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams,
|
||||||
"should have been exactly one call to ValidateToken() by all OIDC upstreams",
|
"should have been exactly one call to ValidateTokenAndMergeWithUserInfo() by all OIDC upstreams",
|
||||||
)
|
)
|
||||||
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
|
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
|
||||||
"ValidateToken() was called on the wrong OIDC upstream",
|
"ValidateTokenAndMergeWithUserInfo() was called on the wrong OIDC upstream",
|
||||||
)
|
)
|
||||||
require.Equal(t, expectedArgs, actualArgs)
|
require.Equal(t, expectedArgs, actualArgs)
|
||||||
}
|
}
|
||||||
@ -547,10 +562,10 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToValidateToken(t *tes
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
actualCallCountAcrossAllOIDCUpstreams := 0
|
actualCallCountAcrossAllOIDCUpstreams := 0
|
||||||
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
||||||
actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.validateTokenCallCount
|
actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.validateTokenAndMergeWithUserInfoCallCount
|
||||||
}
|
}
|
||||||
require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams,
|
require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams,
|
||||||
"expected exactly zero calls to ValidateToken()",
|
"expected exactly zero calls to ValidateTokenAndMergeWithUserInfo()",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,24 +611,26 @@ func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TestUpstreamOIDCIdentityProviderBuilder struct {
|
type TestUpstreamOIDCIdentityProviderBuilder struct {
|
||||||
name string
|
name string
|
||||||
resourceUID types.UID
|
resourceUID types.UID
|
||||||
clientID string
|
clientID string
|
||||||
scopes []string
|
scopes []string
|
||||||
idToken map[string]interface{}
|
idToken map[string]interface{}
|
||||||
refreshToken *oidctypes.RefreshToken
|
refreshToken *oidctypes.RefreshToken
|
||||||
usernameClaim string
|
accessToken *oidctypes.AccessToken
|
||||||
groupsClaim string
|
usernameClaim string
|
||||||
refreshedTokens *oauth2.Token
|
groupsClaim string
|
||||||
validatedTokens *oidctypes.Token
|
refreshedTokens *oauth2.Token
|
||||||
authorizationURL url.URL
|
validatedAndMergedWithUserInfoTokens *oidctypes.Token
|
||||||
additionalAuthcodeParams map[string]string
|
authorizationURL url.URL
|
||||||
allowPasswordGrant bool
|
hasUserInfoURL bool
|
||||||
authcodeExchangeErr error
|
additionalAuthcodeParams map[string]string
|
||||||
passwordGrantErr error
|
allowPasswordGrant bool
|
||||||
performRefreshErr error
|
authcodeExchangeErr error
|
||||||
revokeTokenErr error
|
passwordGrantErr error
|
||||||
validateTokenErr error
|
performRefreshErr error
|
||||||
|
revokeTokenErr error
|
||||||
|
validateTokenAndMergeWithUserInfoErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder {
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
@ -636,6 +653,16 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAuthorizationURL(value url
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUserInfoURL() *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
|
u.hasUserInfoURL = true
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutUserInfoURL() *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
|
u.hasUserInfoURL = false
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAllowPasswordGrant(value bool) *TestUpstreamOIDCIdentityProviderBuilder {
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAllowPasswordGrant(value bool) *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
u.allowPasswordGrant = value
|
u.allowPasswordGrant = value
|
||||||
return u
|
return u
|
||||||
@ -699,6 +726,20 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutRefreshToken() *TestUps
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAccessToken(token string) *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
|
u.accessToken = &oidctypes.AccessToken{Token: token}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithEmptyAccessToken() *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
|
u.accessToken = &oidctypes.AccessToken{Token: ""}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutAccessToken() *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
|
u.accessToken = nil
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
u.authcodeExchangeErr = err
|
u.authcodeExchangeErr = err
|
||||||
return u
|
return u
|
||||||
@ -719,13 +760,13 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPerformRefreshError(err er
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidatedTokens(tokens *oidctypes.Token) *TestUpstreamOIDCIdentityProviderBuilder {
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidatedAndMergedWithUserInfoTokens(tokens *oidctypes.Token) *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
u.validatedTokens = tokens
|
u.validatedAndMergedWithUserInfoTokens = tokens
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidateTokenError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
|
func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidateTokenAndMergeWithUserInfoError(err error) *TestUpstreamOIDCIdentityProviderBuilder {
|
||||||
u.validateTokenErr = err
|
u.validateTokenAndMergeWithUserInfoErr = err
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -744,18 +785,19 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent
|
|||||||
Scopes: u.scopes,
|
Scopes: u.scopes,
|
||||||
AllowPasswordGrant: u.allowPasswordGrant,
|
AllowPasswordGrant: u.allowPasswordGrant,
|
||||||
AuthorizationURL: u.authorizationURL,
|
AuthorizationURL: u.authorizationURL,
|
||||||
|
UserInfoURL: u.hasUserInfoURL,
|
||||||
AdditionalAuthcodeParams: u.additionalAuthcodeParams,
|
AdditionalAuthcodeParams: u.additionalAuthcodeParams,
|
||||||
ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
|
ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
|
||||||
if u.authcodeExchangeErr != nil {
|
if u.authcodeExchangeErr != nil {
|
||||||
return nil, u.authcodeExchangeErr
|
return nil, u.authcodeExchangeErr
|
||||||
}
|
}
|
||||||
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken}, nil
|
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken, AccessToken: u.accessToken}, nil
|
||||||
},
|
},
|
||||||
PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) {
|
PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) {
|
||||||
if u.passwordGrantErr != nil {
|
if u.passwordGrantErr != nil {
|
||||||
return nil, u.passwordGrantErr
|
return nil, u.passwordGrantErr
|
||||||
}
|
}
|
||||||
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken}, nil
|
return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken, AccessToken: u.accessToken}, nil
|
||||||
},
|
},
|
||||||
PerformRefreshFunc: func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
|
PerformRefreshFunc: func(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
|
||||||
if u.performRefreshErr != nil {
|
if u.performRefreshErr != nil {
|
||||||
@ -766,11 +808,11 @@ func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdent
|
|||||||
RevokeTokenFunc: func(ctx context.Context, refreshToken string, tokenType provider.RevocableTokenType) error {
|
RevokeTokenFunc: func(ctx context.Context, refreshToken string, tokenType provider.RevocableTokenType) error {
|
||||||
return u.revokeTokenErr
|
return u.revokeTokenErr
|
||||||
},
|
},
|
||||||
ValidateTokenFunc: func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
|
ValidateTokenAndMergeWithUserInfoFunc: func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
|
||||||
if u.validateTokenErr != nil {
|
if u.validateTokenAndMergeWithUserInfoErr != nil {
|
||||||
return nil, u.validateTokenErr
|
return nil, u.validateTokenAndMergeWithUserInfoErr
|
||||||
}
|
}
|
||||||
return u.validatedTokens, nil
|
return u.validatedAndMergedWithUserInfoTokens, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1003,7 +1045,7 @@ func validateAuthcodeStorage(
|
|||||||
require.Empty(t, actualClaims.CodeHash)
|
require.Empty(t, actualClaims.CodeHash)
|
||||||
require.Empty(t, actualClaims.AccessTokenHash)
|
require.Empty(t, actualClaims.AccessTokenHash)
|
||||||
require.Empty(t, actualClaims.AuthenticationContextClassReference)
|
require.Empty(t, actualClaims.AuthenticationContextClassReference)
|
||||||
require.Empty(t, actualClaims.AuthenticationMethodsReference)
|
require.Empty(t, actualClaims.AuthenticationMethodsReferences)
|
||||||
|
|
||||||
// Check that the custom Pinniped session data matches.
|
// Check that the custom Pinniped session data matches.
|
||||||
require.Equal(t, wantCustomSessionData, storedSessionFromAuthcode.Custom)
|
require.Equal(t, wantCustomSessionData, storedSessionFromAuthcode.Custom)
|
||||||
|
@ -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 testutil
|
package testutil
|
||||||
@ -29,6 +29,8 @@ func NewFakePinnipedSession() *psession.PinnipedSession {
|
|||||||
ProviderName: "fake-provider-name",
|
ProviderName: "fake-provider-name",
|
||||||
OIDC: &psession.OIDCSessionData{
|
OIDC: &psession.OIDCSessionData{
|
||||||
UpstreamRefreshToken: "fake-upstream-refresh-token",
|
UpstreamRefreshToken: "fake-upstream-refresh-token",
|
||||||
|
UpstreamSubject: "some-subject",
|
||||||
|
UpstreamIssuer: "some-issuer",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
168
internal/testutil/testlogger/stdr_copied.go
Normal file
168
internal/testutil/testlogger/stdr_copied.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package testlogger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/go-logr/stdr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newStdLogger returns a logr.Logger that matches the legacy v0.4.0 stdr.New implementation.
|
||||||
|
// All unnecessary functionality has been stripped out. Avoid using this if possible.
|
||||||
|
func newStdLogger(std stdr.StdLogger) logr.Logger {
|
||||||
|
return logr.New(logger{
|
||||||
|
std: std,
|
||||||
|
prefix: "",
|
||||||
|
values: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
std stdr.StdLogger
|
||||||
|
prefix string
|
||||||
|
values []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logger) clone() logger {
|
||||||
|
out := l
|
||||||
|
l.values = copySlice(l.values)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func copySlice(in []interface{}) []interface{} {
|
||||||
|
out := make([]interface{}, len(in))
|
||||||
|
copy(out, in)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Magic string for intermediate frames that we should ignore.
|
||||||
|
const autogeneratedFrameName = "<autogenerated>"
|
||||||
|
|
||||||
|
// Discover how many frames we need to climb to find the caller. This approach
|
||||||
|
// was suggested by Ian Lance Taylor of the Go team, so it *should* be safe
|
||||||
|
// enough (famous last words).
|
||||||
|
func framesToCaller() int {
|
||||||
|
// 1 is the immediate caller. 3 should be too many.
|
||||||
|
for i := 1; i < 3; i++ {
|
||||||
|
_, file, _, _ := runtime.Caller(i + 1) // +1 for this function's frame
|
||||||
|
if file != autogeneratedFrameName {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1 // something went wrong, this is safe
|
||||||
|
}
|
||||||
|
|
||||||
|
func flatten(kvList ...interface{}) string {
|
||||||
|
keys := make([]string, 0, len(kvList))
|
||||||
|
vals := make(map[string]interface{}, len(kvList))
|
||||||
|
for i := 0; i < len(kvList); i += 2 {
|
||||||
|
k, ok := kvList[i].(string)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("key is not a string: %s", pretty(kvList[i])))
|
||||||
|
}
|
||||||
|
var v interface{}
|
||||||
|
if i+1 < len(kvList) {
|
||||||
|
v = kvList[i+1]
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
vals[k] = v
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
for i, k := range keys {
|
||||||
|
v := vals[k]
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buf.WriteString(pretty(k))
|
||||||
|
buf.WriteString("=")
|
||||||
|
buf.WriteString(pretty(v))
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pretty(value interface{}) string {
|
||||||
|
jb, _ := json.Marshal(value)
|
||||||
|
return string(jb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logger) Info(level int, msg string, kvList ...interface{}) {
|
||||||
|
if l.Enabled(level) {
|
||||||
|
builtin := make([]interface{}, 0, 4)
|
||||||
|
builtin = append(builtin, "level", level, "msg", msg)
|
||||||
|
builtinStr := flatten(builtin...)
|
||||||
|
fixedStr := flatten(l.values...)
|
||||||
|
userStr := flatten(kvList...)
|
||||||
|
l.output(framesToCaller(), fmt.Sprintln(l.prefix, builtinStr, fixedStr, userStr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logger) Enabled(level int) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logger) Error(err error, msg string, kvList ...interface{}) {
|
||||||
|
builtin := make([]interface{}, 0, 4)
|
||||||
|
builtin = append(builtin, "msg", msg)
|
||||||
|
builtinStr := flatten(builtin...)
|
||||||
|
var loggableErr interface{}
|
||||||
|
if err != nil {
|
||||||
|
loggableErr = err.Error()
|
||||||
|
}
|
||||||
|
errStr := flatten("error", loggableErr)
|
||||||
|
fixedStr := flatten(l.values...)
|
||||||
|
userStr := flatten(kvList...)
|
||||||
|
l.output(framesToCaller(), fmt.Sprintln(l.prefix, builtinStr, errStr, fixedStr, userStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logger) output(calldepth int, s string) {
|
||||||
|
depth := calldepth + 2 // offset for this adapter
|
||||||
|
|
||||||
|
// ignore errors - what can we really do about them?
|
||||||
|
if l.std != nil {
|
||||||
|
_ = l.std.Output(depth, s)
|
||||||
|
} else {
|
||||||
|
_ = log.Output(depth, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logger) V(level int) logr.LogSink {
|
||||||
|
return l.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithName returns a new logr.Logger with the specified name appended. stdr
|
||||||
|
// uses '/' characters to separate name elements. Callers should not pass '/'
|
||||||
|
// in the provided name string, but this library does not actually enforce that.
|
||||||
|
func (l logger) WithName(name string) logr.LogSink {
|
||||||
|
new := l.clone()
|
||||||
|
if len(l.prefix) > 0 {
|
||||||
|
new.prefix = l.prefix + "/"
|
||||||
|
}
|
||||||
|
new.prefix += name
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValues returns a new logr.Logger with the specified key-and-values
|
||||||
|
// saved.
|
||||||
|
func (l logger) WithValues(kvList ...interface{}) logr.LogSink {
|
||||||
|
new := l.clone()
|
||||||
|
new.values = append(new.values, kvList...)
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logger) WithCallDepth(depth int) logr.LogSink {
|
||||||
|
return l.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ logr.LogSink = logger{}
|
||||||
|
var _ logr.CallDepthLogSink = logger{}
|
||||||
|
|
||||||
|
func (l logger) Init(info logr.RuntimeInfo) {}
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright 2020 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 testlogger implements a logr.Logger suitable for writing test assertions.
|
// Package testlogger wraps logr.Logger to allow for writing test assertions.
|
||||||
package testlogger
|
package testlogger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -17,20 +17,27 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger implements logr.Logger in a way that captures logs for test assertions.
|
// Logger wraps logr.Logger in a way that captures logs for test assertions.
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
logr.Logger
|
Logger logr.Logger
|
||||||
t *testing.T
|
t *testing.T
|
||||||
buffer syncBuffer
|
buffer syncBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new test Logger.
|
// New returns a new test Logger. Use this for all new tests.
|
||||||
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.
|
||||||
|
func NewLegacy(t *testing.T) *Logger {
|
||||||
|
res := New(t)
|
||||||
|
res.Logger = newStdLogger(log.New(&res.buffer, "", 0))
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Lines returns the lines written to the test logger.
|
// Lines returns the lines written to the test logger.
|
||||||
func (l *Logger) Lines() []string {
|
func (l *Logger) Lines() []string {
|
||||||
l.t.Helper()
|
l.t.Helper()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020 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
|
||||||
@ -17,7 +17,7 @@ type TranscriptLogger struct {
|
|||||||
transcript []TranscriptLogMessage
|
transcript []TranscriptLogMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ logr.Logger = &TranscriptLogger{}
|
var _ logr.LogSink = &TranscriptLogger{}
|
||||||
|
|
||||||
type TranscriptLogMessage struct {
|
type TranscriptLogMessage struct {
|
||||||
Level string
|
Level string
|
||||||
@ -36,7 +36,7 @@ func (log *TranscriptLogger) Transcript() []TranscriptLogMessage {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (log *TranscriptLogger) Info(msg string, keysAndValues ...interface{}) {
|
func (log *TranscriptLogger) Info(level int, msg string, keysAndValues ...interface{}) {
|
||||||
log.lock.Lock()
|
log.lock.Lock()
|
||||||
defer log.lock.Unlock()
|
defer log.lock.Unlock()
|
||||||
log.transcript = append(log.transcript, TranscriptLogMessage{
|
log.transcript = append(log.transcript, TranscriptLogMessage{
|
||||||
@ -54,18 +54,20 @@ func (log *TranscriptLogger) Error(_ error, msg string, _ ...interface{}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*TranscriptLogger) Enabled() bool {
|
func (log *TranscriptLogger) Enabled(level int) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (log *TranscriptLogger) V(_ int) logr.Logger {
|
func (log *TranscriptLogger) V(_ int) logr.LogSink {
|
||||||
return log
|
return log
|
||||||
}
|
}
|
||||||
|
|
||||||
func (log *TranscriptLogger) WithName(_ string) logr.Logger {
|
func (log *TranscriptLogger) WithName(_ string) logr.LogSink {
|
||||||
return log
|
return log
|
||||||
}
|
}
|
||||||
|
|
||||||
func (log *TranscriptLogger) WithValues(_ ...interface{}) logr.Logger {
|
func (log *TranscriptLogger) WithValues(_ ...interface{}) logr.LogSink {
|
||||||
return log
|
return log
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (log *TranscriptLogger) Init(info logr.RuntimeInfo) {}
|
||||||
|
@ -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 upstreamldap implements an abstraction of upstream LDAP IDP interactions.
|
// Package upstreamldap implements an abstraction of upstream LDAP IDP interactions.
|
||||||
@ -13,13 +13,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/google/uuid"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/utils/trace"
|
"k8s.io/utils/trace"
|
||||||
@ -39,7 +37,6 @@ const (
|
|||||||
groupSearchPageSize = uint32(250)
|
groupSearchPageSize = uint32(250)
|
||||||
defaultLDAPPort = uint16(389)
|
defaultLDAPPort = uint16(389)
|
||||||
defaultLDAPSPort = uint16(636)
|
defaultLDAPSPort = uint16(636)
|
||||||
sAMAccountNameAttribute = "sAMAccountName"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conn abstracts the upstream LDAP communication protocol (mostly for testing).
|
// Conn abstracts the upstream LDAP communication protocol (mostly for testing).
|
||||||
@ -119,6 +116,9 @@ type ProviderConfig struct {
|
|||||||
// GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group
|
// GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group
|
||||||
// name when it comes out of LDAP.
|
// name when it comes out of LDAP.
|
||||||
GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error)
|
GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error)
|
||||||
|
|
||||||
|
// RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected.
|
||||||
|
RefreshAttributeChecks map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
|
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
|
||||||
@ -170,9 +170,11 @@ func (p *Provider) GetConfig() ProviderConfig {
|
|||||||
return p.c
|
return p.c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes provider.StoredRefreshAttributes) error {
|
||||||
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
||||||
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
||||||
|
userDN := storedRefreshAttributes.DN
|
||||||
|
|
||||||
searchResult, err := p.performRefresh(ctx, userDN)
|
searchResult, err := p.performRefresh(ctx, userDN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.traceRefreshFailure(t, err)
|
p.traceRefreshFailure(t, err)
|
||||||
@ -182,23 +184,23 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
|
|||||||
// if any more or less than one entry, error.
|
// if any more or less than one entry, error.
|
||||||
// we don't need to worry about logging this because we know it's a dn.
|
// we don't need to worry about logging this because we know it's a dn.
|
||||||
if len(searchResult.Entries) != 1 {
|
if len(searchResult.Entries) != 1 {
|
||||||
return fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`,
|
return fmt.Errorf(`searching for user %q resulted in %d search results, but expected 1 result`,
|
||||||
userDN, len(searchResult.Entries),
|
userDN, len(searchResult.Entries),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
userEntry := searchResult.Entries[0]
|
userEntry := searchResult.Entries[0]
|
||||||
if len(userEntry.DN) == 0 {
|
if len(userEntry.DN) == 0 {
|
||||||
return fmt.Errorf(`searching for user with original DN "%s" resulted in search result without DN`, userDN)
|
return fmt.Errorf(`searching for user with original DN %q resulted in search result without DN`, userDN)
|
||||||
}
|
}
|
||||||
|
|
||||||
newUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, userDN)
|
newUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, userDN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if newUsername != expectedUsername {
|
if newUsername != storedRefreshAttributes.Username {
|
||||||
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
|
return fmt.Errorf(`searching for user %q returned a different username than the previous value. expected: %q, actual: %q`,
|
||||||
userDN, expectedUsername, newUsername,
|
userDN, storedRefreshAttributes.Username, newUsername,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,10 +209,15 @@ func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
||||||
if newSubject != expectedSubject {
|
if newSubject != storedRefreshAttributes.Subject {
|
||||||
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, expectedSubject, newSubject)
|
return fmt.Errorf(`searching for user %q produced a different subject than the previous value. expected: %q, actual: %q`, userDN, storedRefreshAttributes.Subject, newSubject)
|
||||||
|
}
|
||||||
|
for attribute, validateFunc := range p.c.RefreshAttributeChecks {
|
||||||
|
err = validateFunc(userEntry, storedRefreshAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(`validation for attribute %q failed during upstream refresh: %w`, attribute, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we checked that the user still exists and their information is the same, so just return.
|
// we checked that the user still exists and their information is the same, so just return.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -220,19 +227,19 @@ func (p *Provider) performRefresh(ctx context.Context, userDN string) (*ldap.Sea
|
|||||||
|
|
||||||
conn, err := p.dial(ctx)
|
conn, err := p.dial(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err)
|
return nil, fmt.Errorf(`error dialing host %q: %w`, p.c.Host, err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
|
return nil, fmt.Errorf(`error binding as %q before user search: %w`, p.c.BindUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
searchResult, err := conn.Search(search)
|
searchResult, err := conn.Search(search)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(`error searching for user "%s": %w`, userDN, err)
|
return nil, fmt.Errorf(`error searching for user %q: %w`, userDN, err)
|
||||||
}
|
}
|
||||||
return searchResult, nil
|
return searchResult, nil
|
||||||
}
|
}
|
||||||
@ -362,13 +369,13 @@ func (p *Provider) TestConnection(ctx context.Context) error {
|
|||||||
|
|
||||||
conn, err := p.dial(ctx)
|
conn, err := p.dial(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err)
|
return fmt.Errorf(`error dialing host %q: %w`, p.c.Host, err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(`error binding as "%s": %w`, p.c.BindUsername, err)
|
return fmt.Errorf(`error binding as %q: %w`, p.c.BindUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -413,14 +420,14 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi
|
|||||||
conn, err := p.dial(ctx)
|
conn, err := p.dial(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.traceAuthFailure(t, err)
|
p.traceAuthFailure(t, err)
|
||||||
return nil, false, fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err)
|
return nil, false, fmt.Errorf(`error dialing host %q: %w`, p.c.Host, err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.traceAuthFailure(t, err)
|
p.traceAuthFailure(t, err)
|
||||||
return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
|
return nil, false, fmt.Errorf(`error binding as %q before user search: %w`, p.c.BindUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := p.searchAndBindUser(conn, username, bindFunc)
|
response, err := p.searchAndBindUser(conn, username, bindFunc)
|
||||||
@ -448,7 +455,7 @@ func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, er
|
|||||||
groupAttributeName = distinguishedNameAttributeName
|
groupAttributeName = distinguishedNameAttributeName
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := []string{}
|
var groups []string
|
||||||
entries:
|
entries:
|
||||||
for _, groupEntry := range searchResult.Entries {
|
for _, groupEntry := range searchResult.Entries {
|
||||||
if len(groupEntry.DN) == 0 {
|
if len(groupEntry.DN) == 0 {
|
||||||
@ -488,14 +495,14 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e
|
|||||||
conn, err := p.dial(ctx)
|
conn, err := p.dial(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.traceSearchBaseDiscoveryFailure(t, err)
|
p.traceSearchBaseDiscoveryFailure(t, err)
|
||||||
return "", fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err)
|
return "", fmt.Errorf(`error dialing host %q: %w`, p.c.Host, err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.traceSearchBaseDiscoveryFailure(t, err)
|
p.traceSearchBaseDiscoveryFailure(t, err)
|
||||||
return "", fmt.Errorf(`error binding as "%s" before querying for defaultNamingContext: %w`, p.c.BindUsername, err)
|
return "", fmt.Errorf(`error binding as %q before querying for defaultNamingContext: %w`, p.c.BindUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
searchResult, err := conn.Search(p.defaultNamingContextRequest())
|
searchResult, err := conn.Search(p.defaultNamingContextRequest())
|
||||||
@ -539,13 +546,13 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
// At this point, we have matched at least one entry, so we can be confident that the username is not actually
|
// At this point, we have matched at least one entry, so we can be confident that the username is not actually
|
||||||
// someone's password mistakenly entered into the username field, so we can log it without concern.
|
// someone's password mistakenly entered into the username field, so we can log it without concern.
|
||||||
if len(searchResult.Entries) > 1 {
|
if len(searchResult.Entries) > 1 {
|
||||||
return nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`,
|
return nil, fmt.Errorf(`searching for user %q resulted in %d search results, but expected 1 result`,
|
||||||
username, len(searchResult.Entries),
|
username, len(searchResult.Entries),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
userEntry := searchResult.Entries[0]
|
userEntry := searchResult.Entries[0]
|
||||||
if len(userEntry.DN) == 0 {
|
if len(userEntry.DN) == 0 {
|
||||||
return nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username)
|
return nil, fmt.Errorf(`searching for user %q resulted in search result without DN`, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username)
|
mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username)
|
||||||
@ -560,7 +567,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mappedGroupNames := []string{}
|
var mappedGroupNames []string
|
||||||
if len(p.c.GroupSearch.Base) > 0 {
|
if len(p.c.GroupSearch.Base) > 0 {
|
||||||
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -569,6 +576,15 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
}
|
}
|
||||||
sort.Strings(mappedGroupNames)
|
sort.Strings(mappedGroupNames)
|
||||||
|
|
||||||
|
mappedRefreshAttributes := make(map[string]string)
|
||||||
|
for k := range p.c.RefreshAttributeChecks {
|
||||||
|
mappedVal, err := p.getSearchResultAttributeRawValueEncoded(k, userEntry, username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mappedRefreshAttributes[k] = mappedVal
|
||||||
|
}
|
||||||
|
|
||||||
// Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername!
|
// Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername!
|
||||||
err = bindFunc(conn, userEntry.DN)
|
err = bindFunc(conn, userEntry.DN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -578,7 +594,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
|
if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err)
|
return nil, fmt.Errorf(`error binding for user %q using provided password against DN %q: %w`, username, userEntry.DN, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(mappedUsername) == 0 || len(mappedUID) == 0 {
|
if len(mappedUsername) == 0 || len(mappedUID) == 0 {
|
||||||
@ -592,7 +608,8 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
|||||||
UID: mappedUID,
|
UID: mappedUID,
|
||||||
Groups: mappedGroupNames,
|
Groups: mappedGroupNames,
|
||||||
},
|
},
|
||||||
DN: userEntry.DN,
|
DN: userEntry.DN,
|
||||||
|
ExtraRefreshAttributes: mappedRefreshAttributes,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
@ -658,13 +675,16 @@ func (p *Provider) refreshUserSearchRequest(dn string) *ldap.SearchRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) userSearchRequestedAttributes() []string {
|
func (p *Provider) userSearchRequestedAttributes() []string {
|
||||||
attributes := []string{}
|
attributes := make([]string, 0, len(p.c.RefreshAttributeChecks)+2)
|
||||||
if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName {
|
if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName {
|
||||||
attributes = append(attributes, p.c.UserSearch.UsernameAttribute)
|
attributes = append(attributes, p.c.UserSearch.UsernameAttribute)
|
||||||
}
|
}
|
||||||
if p.c.UserSearch.UIDAttribute != distinguishedNameAttributeName {
|
if p.c.UserSearch.UIDAttribute != distinguishedNameAttributeName {
|
||||||
attributes = append(attributes, p.c.UserSearch.UIDAttribute)
|
attributes = append(attributes, p.c.UserSearch.UIDAttribute)
|
||||||
}
|
}
|
||||||
|
for k := range p.c.RefreshAttributeChecks {
|
||||||
|
attributes = append(attributes, k)
|
||||||
|
}
|
||||||
return attributes
|
return attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -716,14 +736,14 @@ func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string,
|
|||||||
attributeValues := entry.GetRawAttributeValues(attributeName)
|
attributeValues := entry.GetRawAttributeValues(attributeName)
|
||||||
|
|
||||||
if len(attributeValues) != 1 {
|
if len(attributeValues) != 1 {
|
||||||
return "", fmt.Errorf(`found %d values for attribute "%s" while searching for user "%s", but expected 1 result`,
|
return "", fmt.Errorf(`found %d values for attribute %q while searching for user %q, but expected 1 result`,
|
||||||
len(attributeValues), attributeName, username,
|
len(attributeValues), attributeName, username,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeValue := attributeValues[0]
|
attributeValue := attributeValues[0]
|
||||||
if len(attributeValue) == 0 {
|
if len(attributeValue) == 0 {
|
||||||
return "", fmt.Errorf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`,
|
return "", fmt.Errorf(`found empty value for attribute %q while searching for user %q, but expected value to be non-empty`,
|
||||||
attributeName, username,
|
attributeName, username,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -743,14 +763,14 @@ func (p *Provider) getSearchResultAttributeValue(attributeName string, entry *ld
|
|||||||
attributeValues := entry.GetAttributeValues(attributeName)
|
attributeValues := entry.GetAttributeValues(attributeName)
|
||||||
|
|
||||||
if len(attributeValues) != 1 {
|
if len(attributeValues) != 1 {
|
||||||
return "", fmt.Errorf(`found %d values for attribute "%s" while searching for user "%s", but expected 1 result`,
|
return "", fmt.Errorf(`found %d values for attribute %q while searching for user %q, but expected 1 result`,
|
||||||
len(attributeValues), attributeName, username,
|
len(attributeValues), attributeName, username,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeValue := attributeValues[0]
|
attributeValue := attributeValues[0]
|
||||||
if len(attributeValue) == 0 {
|
if len(attributeValue) == 0 {
|
||||||
return "", fmt.Errorf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`,
|
return "", fmt.Errorf(`found empty value for attribute %q while searching for user %q, but expected value to be non-empty`,
|
||||||
attributeName, username,
|
attributeName, username,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -782,57 +802,18 @@ func (p *Provider) traceRefreshFailure(t *trace.Trace, err error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) {
|
func AttributeUnchangedSinceLogin(attribute string) func(*ldap.Entry, provider.StoredRefreshAttributes) error {
|
||||||
// validation has already been done so we can just get the attribute...
|
return func(entry *ldap.Entry, storedAttributes provider.StoredRefreshAttributes) error {
|
||||||
return func(entry *ldap.Entry) (string, error) {
|
prevAttributeValue := storedAttributes.AdditionalAttributes[attribute]
|
||||||
binaryUUID := entry.GetRawAttributeValue(attributeName)
|
newValues := entry.GetRawAttributeValues(attribute)
|
||||||
return microsoftUUIDFromBinary(binaryUUID)
|
|
||||||
|
if len(newValues) != 1 {
|
||||||
|
return fmt.Errorf(`expected to find 1 value for %q attribute, but found %d`, attribute, len(newValues))
|
||||||
|
}
|
||||||
|
encodedNewValue := base64.RawURLEncoding.EncodeToString(newValues[0])
|
||||||
|
if prevAttributeValue != encodedNewValue {
|
||||||
|
return fmt.Errorf(`value for attribute %q has changed since initial value at login`, attribute)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func microsoftUUIDFromBinary(binaryUUID []byte) (string, error) {
|
|
||||||
uuidVal, err := uuid.FromBytes(binaryUUID) // start out with the RFC4122 version
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// then swap it because AD stores the first 3 fields little-endian rather than the expected
|
|
||||||
// big-endian.
|
|
||||||
uuidVal[0], uuidVal[1], uuidVal[2], uuidVal[3] = uuidVal[3], uuidVal[2], uuidVal[1], uuidVal[0]
|
|
||||||
uuidVal[4], uuidVal[5] = uuidVal[5], uuidVal[4]
|
|
||||||
uuidVal[6], uuidVal[7] = uuidVal[7], uuidVal[6]
|
|
||||||
return uuidVal.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GroupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) {
|
|
||||||
sAMAccountNameAttributeValues := entry.GetAttributeValues(sAMAccountNameAttribute)
|
|
||||||
|
|
||||||
if len(sAMAccountNameAttributeValues) != 1 {
|
|
||||||
return "", fmt.Errorf(`found %d values for attribute "%s", but expected 1 result`,
|
|
||||||
len(sAMAccountNameAttributeValues), sAMAccountNameAttribute,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sAMAccountName := sAMAccountNameAttributeValues[0]
|
|
||||||
if len(sAMAccountName) == 0 {
|
|
||||||
return "", fmt.Errorf(`found empty value for attribute "%s", but expected value to be non-empty`,
|
|
||||||
sAMAccountNameAttribute,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
distinguishedName := entry.DN
|
|
||||||
domain, err := getDomainFromDistinguishedName(distinguishedName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return sAMAccountName + "@" + domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var domainComponentsRegexp = regexp.MustCompile(",DC=|,dc=")
|
|
||||||
|
|
||||||
func getDomainFromDistinguishedName(distinguishedName string) (string, error) {
|
|
||||||
domainComponents := domainComponentsRegexp.Split(distinguishedName, -1)
|
|
||||||
if len(domainComponents) == 1 {
|
|
||||||
return "", fmt.Errorf("did not find domain components in group dn: %s", distinguishedName)
|
|
||||||
}
|
|
||||||
return strings.Join(domainComponents[1:], "."), nil
|
|
||||||
}
|
|
||||||
|
@ -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 upstreamldap
|
package upstreamldap
|
||||||
@ -26,6 +26,7 @@ import (
|
|||||||
"go.pinniped.dev/internal/crypto/ptls"
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
"go.pinniped.dev/internal/endpointaddr"
|
"go.pinniped.dev/internal/endpointaddr"
|
||||||
"go.pinniped.dev/internal/mocks/mockldapconn"
|
"go.pinniped.dev/internal/mocks/mockldapconn"
|
||||||
|
"go.pinniped.dev/internal/oidc/provider"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
"go.pinniped.dev/internal/testutil/tlsserver"
|
"go.pinniped.dev/internal/testutil/tlsserver"
|
||||||
)
|
)
|
||||||
@ -154,16 +155,17 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
|
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
|
||||||
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticators.Response {
|
expectedAuthResponse := func(editFunc func(r *authenticators.Response)) *authenticators.Response {
|
||||||
u := &user.DefaultInfo{
|
u := &user.DefaultInfo{
|
||||||
Name: testUserSearchResultUsernameAttributeValue,
|
Name: testUserSearchResultUsernameAttributeValue,
|
||||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||||
Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
|
Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
|
||||||
}
|
}
|
||||||
|
response := &authenticators.Response{User: u, DN: testUserSearchResultDNValue, ExtraRefreshAttributes: map[string]string{}}
|
||||||
if editFunc != nil {
|
if editFunc != nil {
|
||||||
editFunc(u)
|
editFunc(response)
|
||||||
}
|
}
|
||||||
return &authenticators.Response{User: u, DN: testUserSearchResultDNValue}
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -250,8 +252,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Groups = []string{}
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Groups = nil
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -282,8 +285,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Name = testUserSearchResultDNValue
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Name = testUserSearchResultDNValue
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -314,8 +318,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue))
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue))
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -337,8 +342,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -360,8 +366,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
info := r.User.(*user.DefaultInfo)
|
||||||
|
info.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -507,33 +514,37 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||||
Groups: []string{"a", "b", "c"},
|
Groups: []string{"a", "b", "c"},
|
||||||
},
|
},
|
||||||
DN: testUserSearchResultDNValue,
|
DN: testUserSearchResultDNValue,
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "override UID parsing to work with microsoft style objectGUIDs",
|
name: "requesting additional refresh related attributes",
|
||||||
username: testUpstreamUsername,
|
username: testUpstreamUsername,
|
||||||
password: testUpstreamPassword,
|
password: testUpstreamPassword,
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
providerConfig: providerConfig(func(p *ProviderConfig) {
|
||||||
p.UIDAttributeParsingOverrides = map[string]func(entry *ldap.Entry) (string, error){
|
p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{
|
||||||
"objectGUID": MicrosoftUUIDFromBinary("objectGUID"),
|
"some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
p.UserSearch.UIDAttribute = "objectGUID"
|
|
||||||
}),
|
}),
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
searchMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
|
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
|
||||||
r.Attributes = []string{testUserSearchUsernameAttribute, "objectGUID"}
|
r.Attributes = append(r.Attributes, "some-attribute-to-check-during-refresh")
|
||||||
})).Return(&ldap.SearchResult{
|
})).Return(&ldap.SearchResult{
|
||||||
Entries: []*ldap.Entry{
|
Entries: []*ldap.Entry{
|
||||||
{
|
{
|
||||||
DN: testUserSearchResultDNValue,
|
DN: testUserSearchResultDNValue,
|
||||||
Attributes: []*ldap.EntryAttribute{
|
Attributes: []*ldap.EntryAttribute{
|
||||||
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
|
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
|
||||||
ldap.NewEntryAttribute("objectGUID", []string{"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"}),
|
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
|
||||||
|
ldap.NewEntryAttribute("some-attribute-to-check-during-refresh", []string{"some-attribute-value"}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}, nil).Times(1)
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
||||||
Return(exampleGroupSearchResult, nil).Times(1)
|
Return(exampleGroupSearchResult, nil).Times(1)
|
||||||
conn.EXPECT().Close().Times(1)
|
conn.EXPECT().Close().Times(1)
|
||||||
@ -541,172 +552,31 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||||
},
|
},
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
|
||||||
r.UID = "04030201-0605-0807-0910-111213141516"
|
r.ExtraRefreshAttributes = map[string]string{"some-attribute-to-check-during-refresh": "c29tZS1hdHRyaWJ1dGUtdmFsdWU"}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "override UID parsing when the attribute name doesn't match what's returned does default parsing",
|
name: "requesting additional refresh related attributes, but they aren't returned",
|
||||||
username: testUpstreamUsername,
|
username: testUpstreamUsername,
|
||||||
password: testUpstreamPassword,
|
password: testUpstreamPassword,
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
providerConfig: providerConfig(func(p *ProviderConfig) {
|
||||||
p.UIDAttributeParsingOverrides = map[string]func(entry *ldap.Entry) (string, error){
|
p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error{
|
||||||
"objectGUID": MicrosoftUUIDFromBinary("objectGUID"),
|
"some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes provider.StoredRefreshAttributes) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
searchMocks: func(conn *mockldapconn.MockConn) {
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
|
||||||
|
r.Attributes = append(r.Attributes, "some-attribute-to-check-during-refresh")
|
||||||
|
})).Return(exampleUserSearchResult, nil).Times(1)
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).
|
||||||
Return(exampleGroupSearchResult, nil).Times(1)
|
Return(exampleGroupSearchResult, nil).Times(1)
|
||||||
conn.EXPECT().Close().Times(1)
|
conn.EXPECT().Close().Times(1)
|
||||||
},
|
},
|
||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
wantError: "found 0 values for attribute \"some-attribute-to-check-during-refresh\" while searching for user \"some-upstream-username\", but expected 1 result",
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
|
||||||
},
|
|
||||||
wantAuthResponse: expectedAuthResponse(nil),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "override group parsing to create new group names",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
|
||||||
},
|
|
||||||
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
|
|
||||||
r.Groups = []string{"Animals@activedirectory.mycompany.example.com", "Mammals@activedirectory.mycompany.example.com"}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "override group parsing when domain can't be determined from dn",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "no-domain-components",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
wantError: "error finding groups for user some-upstream-user-dn: did not find domain components in group dn: no-domain-components",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "override group parsing when entry has multiple values for attribute",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "no-domain-components",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals", "Eukaryotes"}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
wantError: "error finding groups for user some-upstream-user-dn: found 2 values for attribute \"sAMAccountName\", but expected 1 result",
|
|
||||||
}, {
|
|
||||||
name: "override group parsing when entry has no values for attribute",
|
|
||||||
username: testUpstreamUsername,
|
|
||||||
password: testUpstreamPassword,
|
|
||||||
providerConfig: providerConfig(func(p *ProviderConfig) {
|
|
||||||
p.GroupSearch.GroupNameAttribute = "sAMAccountName"
|
|
||||||
p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){
|
|
||||||
"sAMAccountName": GroupSAMAccountNameWithDomainSuffix,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
searchMocks: func(conn *mockldapconn.MockConn) {
|
|
||||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
|
||||||
conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1)
|
|
||||||
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
|
|
||||||
r.Attributes = []string{"sAMAccountName"}
|
|
||||||
}), expectedGroupSearchPageSize).
|
|
||||||
Return(&ldap.SearchResult{
|
|
||||||
Entries: []*ldap.Entry{
|
|
||||||
{
|
|
||||||
DN: "no-domain-components",
|
|
||||||
Attributes: []*ldap.EntryAttribute{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, // note that we are not following referrals at this time
|
|
||||||
Controls: []ldap.Control{},
|
|
||||||
}, nil).Times(1)
|
|
||||||
conn.EXPECT().Close().Times(1)
|
|
||||||
},
|
|
||||||
wantError: "error finding groups for user some-upstream-user-dn: found 0 values for attribute \"sAMAccountName\", but expected 1 result",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "when dial fails",
|
name: "when dial fails",
|
||||||
@ -1162,9 +1032,9 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
provider := New(*tt.providerConfig)
|
ldapProvider := New(*tt.providerConfig)
|
||||||
|
|
||||||
authResponse, authenticated, err := provider.AuthenticateUser(context.Background(), tt.username, tt.password)
|
authResponse, authenticated, err := ldapProvider.AuthenticateUser(context.Background(), tt.username, tt.password)
|
||||||
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
||||||
switch {
|
switch {
|
||||||
case tt.wantError != "":
|
case tt.wantError != "":
|
||||||
@ -1196,7 +1066,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user.
|
// Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user.
|
||||||
|
|
||||||
authResponse, authenticated, err = provider.DryRunAuthenticateUser(context.Background(), tt.username)
|
authResponse, authenticated, err = ldapProvider.DryRunAuthenticateUser(context.Background(), tt.username)
|
||||||
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
require.Equal(t, !tt.wantToSkipDial, dialWasAttempted)
|
||||||
switch {
|
switch {
|
||||||
case tt.wantError != "":
|
case tt.wantError != "":
|
||||||
@ -1217,6 +1087,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpstreamRefresh(t *testing.T) {
|
func TestUpstreamRefresh(t *testing.T) {
|
||||||
|
pwdLastSetAttribute := "pwdLastSet"
|
||||||
expectedUserSearch := &ldap.SearchRequest{
|
expectedUserSearch := &ldap.SearchRequest{
|
||||||
BaseDN: testUserSearchResultDNValue,
|
BaseDN: testUserSearchResultDNValue,
|
||||||
Scope: ldap.ScopeBaseObject,
|
Scope: ldap.ScopeBaseObject,
|
||||||
@ -1225,7 +1096,7 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
TimeLimit: 90,
|
TimeLimit: 90,
|
||||||
TypesOnly: false,
|
TypesOnly: false,
|
||||||
Filter: "(objectClass=*)",
|
Filter: "(objectClass=*)",
|
||||||
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute},
|
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute},
|
||||||
Controls: nil, // don't need paging because we set the SizeLimit so small
|
Controls: nil, // don't need paging because we set the SizeLimit so small
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1242,6 +1113,11 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
Name: testUserSearchUIDAttribute,
|
Name: testUserSearchUIDAttribute,
|
||||||
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: pwdLastSetAttribute,
|
||||||
|
Values: []string{"132801740800000000"},
|
||||||
|
ByteValues: [][]byte{[]byte("132801740800000000")},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1259,6 +1135,9 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
UIDAttribute: testUserSearchUIDAttribute,
|
UIDAttribute: testUserSearchUIDAttribute,
|
||||||
UsernameAttribute: testUserSearchUsernameAttribute,
|
UsernameAttribute: testUserSearchUsernameAttribute,
|
||||||
},
|
},
|
||||||
|
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
|
||||||
|
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -1512,6 +1391,36 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "search result has a changed pwdLastSet value",
|
||||||
|
providerConfig: providerConfig,
|
||||||
|
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||||
|
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||||
|
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: testUserSearchUsernameAttribute,
|
||||||
|
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: testUserSearchUIDAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: pwdLastSetAttribute,
|
||||||
|
ByteValues: [][]byte{[]byte("132801740800000001")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
conn.EXPECT().Close().Times(1)
|
||||||
|
},
|
||||||
|
wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: value for attribute \"pwdLastSet\" has changed since initial value at login",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -1536,9 +1445,15 @@ func TestUpstreamRefresh(t *testing.T) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
provider := New(*providerConfig)
|
initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000"))
|
||||||
|
ldapProvider := New(*providerConfig)
|
||||||
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
|
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
|
||||||
err := provider.PerformRefresh(context.Background(), testUserSearchResultDNValue, testUserSearchResultUsernameAttributeValue, subject)
|
err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{
|
||||||
|
Username: testUserSearchResultUsernameAttributeValue,
|
||||||
|
Subject: subject,
|
||||||
|
DN: testUserSearchResultDNValue,
|
||||||
|
AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded},
|
||||||
|
})
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, tt.wantErr, err.Error())
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
@ -1846,73 +1761,76 @@ func TestRealTLSDialing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetMicrosoftFormattedUUID(t *testing.T) {
|
func TestAttributeUnchangedSinceLogin(t *testing.T) {
|
||||||
|
initialVal := "some-attribute-value"
|
||||||
|
changedVal := "some-different-attribute-value"
|
||||||
|
attributeName := "some-attribute-name"
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
binaryUUID []byte
|
entry *ldap.Entry
|
||||||
wantString string
|
wantResult bool
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "happy path",
|
name: "happy path where value has not changed since login",
|
||||||
binaryUUID: []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"),
|
entry: &ldap.Entry{
|
||||||
wantString: "04030201-0605-0807-0910-111213141516",
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: attributeName,
|
||||||
|
Values: []string{initialVal},
|
||||||
|
ByteValues: [][]byte{[]byte(initialVal)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "not the right length",
|
name: "password has been reset since login",
|
||||||
binaryUUID: []byte("2\xf8\xb0\xaa\xb6V\xb1D\x8b(\xee"),
|
entry: &ldap.Entry{
|
||||||
wantErr: "invalid UUID (got 11 bytes)",
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: attributeName,
|
||||||
|
Values: []string{changedVal},
|
||||||
|
ByteValues: [][]byte{[]byte(changedVal)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "value for attribute \"some-attribute-name\" has changed since initial value at login",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no value for attribute attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{},
|
||||||
|
},
|
||||||
|
wantErr: "expected to find 1 value for \"some-attribute-name\" attribute, but found 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many values for attribute",
|
||||||
|
entry: &ldap.Entry{
|
||||||
|
DN: "some-dn",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: attributeName,
|
||||||
|
ByteValues: [][]byte{[]byte("val1"), []byte("val2")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "expected to find 1 value for \"some-attribute-name\" attribute, but found 2",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
tt := test
|
tt := test
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
actualUUIDString, err := microsoftUUIDFromBinary(tt.binaryUUID)
|
initialValRawEncoded := base64.RawURLEncoding.EncodeToString([]byte(initialVal))
|
||||||
|
err := AttributeUnchangedSinceLogin(attributeName)(tt.entry, provider.StoredRefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}})
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
require.EqualError(t, err, tt.wantErr)
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.Equal(t, tt.wantString, actualUUIDString)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetDomainFromDistinguishedName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
distinguishedName string
|
|
||||||
wantDomain string
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "happy path",
|
|
||||||
distinguishedName: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com",
|
|
||||||
wantDomain: "activedirectory.mycompany.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "lowercased happy path",
|
|
||||||
distinguishedName: "cn=Mammals,ou=Users,ou=pinniped-ad,dc=activedirectory,dc=mycompany,dc=example,dc=com",
|
|
||||||
wantDomain: "activedirectory.mycompany.example.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no domain components",
|
|
||||||
distinguishedName: "not-a-dn",
|
|
||||||
wantErr: "did not find domain components in group dn: not-a-dn",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
tt := test
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
actualDomain, err := getDomainFromDistinguishedName(tt.distinguishedName)
|
|
||||||
if tt.wantErr != "" {
|
|
||||||
require.EqualError(t, err, tt.wantErr)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
require.Equal(t, tt.wantDomain, actualDomain)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 upstreamoidc implements an abstraction of upstream OIDC provider interactions.
|
// Package upstreamoidc implements an abstraction of upstream OIDC provider interactions.
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@ -60,6 +61,19 @@ func (p *ProviderConfig) GetRevocationURL() *url.URL {
|
|||||||
return p.RevocationURL
|
return p.RevocationURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ProviderConfig) HasUserInfoURL() bool {
|
||||||
|
providerJSON := &struct {
|
||||||
|
UserInfoURL string `json:"userinfo_endpoint"`
|
||||||
|
}{}
|
||||||
|
if err := p.Provider.Claims(providerJSON); err != nil {
|
||||||
|
// This should never happen in practice because we should have already successfully
|
||||||
|
// parsed these claims when p.Provider was created.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(providerJSON.UserInfoURL) > 0
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ProviderConfig) GetAdditionalAuthcodeParams() map[string]string {
|
func (p *ProviderConfig) GetAdditionalAuthcodeParams() map[string]string {
|
||||||
return p.AdditionalAuthcodeParams
|
return p.AdditionalAuthcodeParams
|
||||||
}
|
}
|
||||||
@ -112,7 +126,7 @@ func (p *ProviderConfig) PasswordCredentialsGrantAndValidateTokens(ctx context.C
|
|||||||
// There is no nonce to validate for a resource owner password credentials grant because it skips using
|
// There is no nonce to validate for a resource owner password credentials grant because it skips using
|
||||||
// the authorize endpoint and goes straight to the token endpoint.
|
// the authorize endpoint and goes straight to the token endpoint.
|
||||||
const skipNonceValidation nonce.Nonce = ""
|
const skipNonceValidation nonce.Nonce = ""
|
||||||
return p.ValidateToken(ctx, tok, skipNonceValidation)
|
return p.ValidateTokenAndMergeWithUserInfo(ctx, tok, skipNonceValidation, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProviderConfig) ExchangeAuthcodeAndValidateTokens(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce, redirectURI string) (*oidctypes.Token, error) {
|
func (p *ProviderConfig) ExchangeAuthcodeAndValidateTokens(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce, redirectURI string) (*oidctypes.Token, error) {
|
||||||
@ -126,7 +140,7 @@ func (p *ProviderConfig) ExchangeAuthcodeAndValidateTokens(ctx context.Context,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.ValidateToken(ctx, tok, expectedIDTokenNonce)
|
return p.ValidateTokenAndMergeWithUserInfo(ctx, tok, expectedIDTokenNonce, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProviderConfig) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
|
func (p *ProviderConfig) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) {
|
||||||
@ -259,36 +273,27 @@ func (p *ProviderConfig) tryRevokeToken(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateToken will validate the ID token. It will also merge the claims from the userinfo endpoint response,
|
// ValidateTokenAndMergeWithUserInfo will validate the ID token. It will also merge the claims from the userinfo endpoint response,
|
||||||
// if the provider offers the userinfo endpoint.
|
// if the provider offers the userinfo endpoint.
|
||||||
func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
|
func (p *ProviderConfig) ValidateTokenAndMergeWithUserInfo(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, requireIDToken bool, requireUserInfo bool) (*oidctypes.Token, error) {
|
||||||
idTok, hasIDTok := tok.Extra("id_token").(string)
|
var validatedClaims = make(map[string]interface{})
|
||||||
if !hasIDTok {
|
|
||||||
return nil, httperr.New(http.StatusBadRequest, "received response missing ID token")
|
var idTokenExpiry time.Time
|
||||||
}
|
// if we require the id token, make sure we have it.
|
||||||
validated, err := p.Provider.Verifier(&coreosoidc.Config{ClientID: p.GetClientID()}).Verify(coreosoidc.ClientContext(ctx, p.Client), idTok)
|
// also, if it exists but wasn't required, still make sure it passes these checks.
|
||||||
|
idTokenExpiry, idTok, err := p.validateIDToken(ctx, tok, expectedIDTokenNonce, validatedClaims, requireIDToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httperr.Wrap(http.StatusBadRequest, "received invalid ID token", err)
|
return nil, err
|
||||||
}
|
|
||||||
if validated.AccessTokenHash != "" {
|
|
||||||
if err := validated.VerifyAccessToken(tok.AccessToken); err != nil {
|
|
||||||
return nil, httperr.Wrap(http.StatusBadRequest, "received invalid ID token", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if expectedIDTokenNonce != "" {
|
|
||||||
if err := expectedIDTokenNonce.Validate(validated); err != nil {
|
|
||||||
return nil, httperr.Wrap(http.StatusBadRequest, "received ID token with invalid nonce", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var validatedClaims map[string]interface{}
|
idTokenSubject, _ := validatedClaims[oidc.IDTokenSubjectClaim].(string)
|
||||||
if err := validated.Claims(&validatedClaims); err != nil {
|
|
||||||
return nil, httperr.Wrap(http.StatusInternalServerError, "could not unmarshal id token claims", err)
|
|
||||||
}
|
|
||||||
maybeLogClaims("claims from ID token", p.Name, validatedClaims)
|
|
||||||
|
|
||||||
if err := p.maybeFetchUserInfoAndMergeClaims(ctx, tok, validatedClaims); err != nil {
|
if len(idTokenSubject) > 0 || !requireIDToken {
|
||||||
return nil, httperr.Wrap(http.StatusInternalServerError, "could not fetch user info claims", err)
|
// only fetch userinfo if the ID token has a subject or if we are ignoring the id token completely.
|
||||||
|
// otherwise, defer to existing ID token validation
|
||||||
|
if err := p.maybeFetchUserInfoAndMergeClaims(ctx, tok, validatedClaims, requireIDToken, requireUserInfo); err != nil {
|
||||||
|
return nil, httperr.Wrap(http.StatusInternalServerError, "could not fetch user info claims", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &oidctypes.Token{
|
return &oidctypes.Token{
|
||||||
@ -302,58 +307,107 @@ func (p *ProviderConfig) ValidateToken(ctx context.Context, tok *oauth2.Token, e
|
|||||||
},
|
},
|
||||||
IDToken: &oidctypes.IDToken{
|
IDToken: &oidctypes.IDToken{
|
||||||
Token: idTok,
|
Token: idTok,
|
||||||
Expiry: metav1.NewTime(validated.Expiry),
|
Expiry: metav1.NewTime(idTokenExpiry),
|
||||||
Claims: validatedClaims,
|
Claims: validatedClaims,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}) error {
|
func (p *ProviderConfig) validateIDToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, validatedClaims map[string]interface{}, requireIDToken bool) (time.Time, string, error) {
|
||||||
|
idTok, hasIDTok := tok.Extra("id_token").(string)
|
||||||
|
if !hasIDTok && !requireIDToken {
|
||||||
|
return time.Time{}, "", nil // exit early
|
||||||
|
}
|
||||||
|
|
||||||
|
var idTokenExpiry time.Time
|
||||||
|
if !hasIDTok {
|
||||||
|
return time.Time{}, "", httperr.New(http.StatusBadRequest, "received response missing ID token")
|
||||||
|
}
|
||||||
|
validated, err := p.Provider.Verifier(&coreosoidc.Config{ClientID: p.GetClientID()}).Verify(coreosoidc.ClientContext(ctx, p.Client), idTok)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, "", httperr.Wrap(http.StatusBadRequest, "received invalid ID token", err)
|
||||||
|
}
|
||||||
|
if validated.AccessTokenHash != "" {
|
||||||
|
if err := validated.VerifyAccessToken(tok.AccessToken); err != nil {
|
||||||
|
return time.Time{}, "", httperr.Wrap(http.StatusBadRequest, "received invalid ID token", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expectedIDTokenNonce != "" {
|
||||||
|
if err := expectedIDTokenNonce.Validate(validated); err != nil {
|
||||||
|
return time.Time{}, "", httperr.Wrap(http.StatusBadRequest, "received ID token with invalid nonce", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validated.Claims(&validatedClaims); err != nil {
|
||||||
|
return time.Time{}, "", httperr.Wrap(http.StatusInternalServerError, "could not unmarshal id token claims", err)
|
||||||
|
}
|
||||||
|
maybeLogClaims("claims from ID token", p.Name, validatedClaims)
|
||||||
|
idTokenExpiry = validated.Expiry // keep track of the id token expiry if we have an id token. Otherwise, it'll just be the zero value.
|
||||||
|
return idTokenExpiry, idTok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProviderConfig) maybeFetchUserInfoAndMergeClaims(ctx context.Context, tok *oauth2.Token, claims map[string]interface{}, requireIDToken bool, requireUserInfo bool) error {
|
||||||
idTokenSubject, _ := claims[oidc.IDTokenSubjectClaim].(string)
|
idTokenSubject, _ := claims[oidc.IDTokenSubjectClaim].(string)
|
||||||
if len(idTokenSubject) == 0 {
|
|
||||||
return nil // defer to existing ID token validation
|
|
||||||
}
|
|
||||||
|
|
||||||
providerJSON := &struct {
|
userInfo, err := p.maybeFetchUserInfo(ctx, tok, requireUserInfo)
|
||||||
UserInfoURL string `json:"userinfo_endpoint"`
|
if err != nil {
|
||||||
}{}
|
return err
|
||||||
if err := p.Provider.Claims(providerJSON); err != nil {
|
|
||||||
// this should never happen because we should have already parsed these claims at an earlier stage
|
|
||||||
return httperr.Wrap(http.StatusInternalServerError, "could not unmarshal discovery JSON", err)
|
|
||||||
}
|
}
|
||||||
|
if userInfo == nil {
|
||||||
// implementing the user info endpoint is not required, skip this logic when it is absent
|
|
||||||
if len(providerJSON.UserInfoURL) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfo, err := p.Provider.UserInfo(coreosoidc.ClientContext(ctx, p.Client), oauth2.StaticTokenSource(tok))
|
|
||||||
if err != nil {
|
|
||||||
return httperr.Wrap(http.StatusInternalServerError, "could not get user info", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The sub (subject) Claim MUST always be returned in the UserInfo Response.
|
// The sub (subject) Claim MUST always be returned in the UserInfo Response.
|
||||||
//
|
|
||||||
// NOTE: Due to the possibility of token substitution attacks (see Section 16.11), the UserInfo Response is not
|
// NOTE: Due to the possibility of token substitution attacks (see Section 16.11), the UserInfo Response is not
|
||||||
// guaranteed to be about the End-User identified by the sub (subject) element of the ID Token. The sub Claim in
|
// guaranteed to be about the End-User identified by the sub (subject) element of the ID Token. The sub Claim in
|
||||||
// the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token; if they do not match,
|
// the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token; if they do not match,
|
||||||
// the UserInfo Response values MUST NOT be used.
|
// the UserInfo Response values MUST NOT be used.
|
||||||
//
|
//
|
||||||
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||||
if len(userInfo.Subject) == 0 || userInfo.Subject != idTokenSubject {
|
// If there is no ID token and it is not required, we must assume that the caller is performing other checks
|
||||||
|
// to ensure the subject is correct.
|
||||||
|
checkIDToken := requireIDToken || len(idTokenSubject) > 0
|
||||||
|
if checkIDToken && (len(userInfo.Subject) == 0 || userInfo.Subject != idTokenSubject) {
|
||||||
return httperr.Newf(http.StatusUnprocessableEntity, "userinfo 'sub' claim (%s) did not match id_token 'sub' claim (%s)", userInfo.Subject, idTokenSubject)
|
return httperr.Newf(http.StatusUnprocessableEntity, "userinfo 'sub' claim (%s) did not match id_token 'sub' claim (%s)", userInfo.Subject, idTokenSubject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep track of the issuer from the ID token
|
||||||
|
idTokenIssuer := claims["iss"]
|
||||||
|
|
||||||
// merge existing claims with user info claims
|
// merge existing claims with user info claims
|
||||||
if err := userInfo.Claims(&claims); err != nil {
|
if err := userInfo.Claims(&claims); err != nil {
|
||||||
return httperr.Wrap(http.StatusInternalServerError, "could not unmarshal user info claims", err)
|
return httperr.Wrap(http.StatusInternalServerError, "could not unmarshal user info claims", err)
|
||||||
}
|
}
|
||||||
|
// The OIDC spec for the UserInfo response does not make any guarantees about the iss claim's existence or validity:
|
||||||
|
// "If signed, the UserInfo Response SHOULD contain the Claims iss (issuer) and aud (audience) as members. The iss value SHOULD be the OP's Issuer Identifier URL."
|
||||||
|
// See https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||||
|
// So we just ignore it and use it the version from the id token, which has stronger guarantees.
|
||||||
|
delete(claims, "iss")
|
||||||
|
if idTokenIssuer != nil {
|
||||||
|
claims["iss"] = idTokenIssuer
|
||||||
|
}
|
||||||
|
|
||||||
maybeLogClaims("claims from ID token and userinfo", p.Name, claims)
|
maybeLogClaims("claims from ID token and userinfo", p.Name, claims)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ProviderConfig) maybeFetchUserInfo(ctx context.Context, tok *oauth2.Token, requireUserInfo bool) (*coreosoidc.UserInfo, error) {
|
||||||
|
// implementing the user info endpoint is not required by the OIDC spec, but we may require it in certain situations.
|
||||||
|
if !p.HasUserInfoURL() {
|
||||||
|
if requireUserInfo {
|
||||||
|
// TODO should these all be http errors?
|
||||||
|
return nil, httperr.New(http.StatusInternalServerError, "userinfo endpoint not found, but is required")
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo, err := p.Provider.UserInfo(coreosoidc.ClientContext(ctx, p.Client), oauth2.StaticTokenSource(tok))
|
||||||
|
if err != nil {
|
||||||
|
return nil, httperr.Wrap(http.StatusInternalServerError, "could not get user info", err)
|
||||||
|
}
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -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 upstreamoidc
|
package upstreamoidc
|
||||||
@ -41,6 +41,9 @@ func TestProviderConfig(t *testing.T) {
|
|||||||
Endpoint: oauth2.Endpoint{AuthURL: "https://example.com"},
|
Endpoint: oauth2.Endpoint{AuthURL: "https://example.com"},
|
||||||
Scopes: []string{"scope1", "scope2"},
|
Scopes: []string{"scope1", "scope2"},
|
||||||
},
|
},
|
||||||
|
Provider: &mockProvider{
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "https://example.com/userinfo"}`),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
require.Equal(t, "test-name", p.GetName())
|
require.Equal(t, "test-name", p.GetName())
|
||||||
require.Equal(t, "test-client-id", p.GetClientID())
|
require.Equal(t, "test-client-id", p.GetClientID())
|
||||||
@ -55,6 +58,16 @@ func TestProviderConfig(t *testing.T) {
|
|||||||
require.True(t, p.AllowsPasswordGrant())
|
require.True(t, p.AllowsPasswordGrant())
|
||||||
p.AllowPasswordGrant = false
|
p.AllowPasswordGrant = false
|
||||||
require.False(t, p.AllowsPasswordGrant())
|
require.False(t, p.AllowsPasswordGrant())
|
||||||
|
|
||||||
|
require.True(t, p.HasUserInfoURL())
|
||||||
|
p.Provider = &mockProvider{
|
||||||
|
rawClaims: []byte(`{"some_other_endpoint": "https://example.com/blah"}`),
|
||||||
|
}
|
||||||
|
require.False(t, p.HasUserInfoURL())
|
||||||
|
p.Provider = &mockProvider{
|
||||||
|
rawClaims: []byte(`{`),
|
||||||
|
}
|
||||||
|
require.False(t, p.HasUserInfoURL())
|
||||||
})
|
})
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -707,6 +720,399 @@ func TestProviderConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ValidateTokenAndMergeWithUserInfo", func(t *testing.T) {
|
||||||
|
expiryTime := time.Now().Add(42 * time.Second)
|
||||||
|
testTokenWithoutIDToken := &oauth2.Token{
|
||||||
|
AccessToken: "test-access-token",
|
||||||
|
// the library sets the original refresh token into the result, even though the server did not return that
|
||||||
|
RefreshToken: "test-initial-refresh-token",
|
||||||
|
TokenType: "test-token-type",
|
||||||
|
Expiry: expiryTime,
|
||||||
|
}
|
||||||
|
// generated from jwt.io
|
||||||
|
// sub: some-subject
|
||||||
|
// iss: some-issuer
|
||||||
|
// nonce: some-nonce
|
||||||
|
goodIDToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLXN1YmplY3QiLCJub25jZSI6InNvbWUtbm9uY2UiLCJpc3MiOiJzb21lLWlzc3VlciJ9.eGvzOihLUqzn3M4k6fHsToedgy7Fu89_Xu_u4mwMgRlIyRWZqmEMV76RVLnZd9Ihm9j_VpvrpirIkaj4JM9eRNfLX1n328cmBivBwnTKAzHuTm17dUKO5EvdTmQzmwnN0WZ8nWk4GfR7RzcvE1V8G9tIiWD8FkO3Dr-NR_zTun3N37onAazVLCmF0SDtATDfUH1ETqviHEp8xGx5HD5mv5T3HEjOuer5gxTEnfncef0LurBH3po-C0tXHKu74PD8x88CMJ1DLsRdCalnctwa850slKPkBSTP-ssh0JVg7cdMXoosVpwiXtKYaBkrhu8VS018aFP-cBbW0mYwsHmt3g" //nolint:gosec
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tok *oauth2.Token
|
||||||
|
nonce nonce.Nonce
|
||||||
|
requireIDToken bool
|
||||||
|
requireUserInfo bool
|
||||||
|
userInfo *oidc.UserInfo
|
||||||
|
rawClaims []byte
|
||||||
|
userInfoErr error
|
||||||
|
wantErr string
|
||||||
|
wantMergedTokens *oidctypes.Token
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "token with id, access and refresh tokens, valid nonce, and no userinfo",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: goodIDToken,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer",
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
"sub": "some-subject",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "id token not required but is provided",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: false,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "sub": "some-subject"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: goodIDToken,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer",
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
"sub": "some-subject",
|
||||||
|
"name": "Pinny TheSeal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token with id, access and refresh tokens, valid nonce, and userinfo with a value that doesn't exist in the id token",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "sub": "some-subject"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: goodIDToken,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer",
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
"sub": "some-subject",
|
||||||
|
"name": "Pinny TheSeal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "userinfo is required, token with id, access and refresh tokens, valid nonce, and userinfo with a value that doesn't exist in the id token",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
requireUserInfo: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "sub": "some-subject"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: goodIDToken,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer",
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
"sub": "some-subject",
|
||||||
|
"name": "Pinny TheSeal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claims from userinfo override id token claims",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLXN1YmplY3QiLCJuYW1lIjoiSm9obiBEb2UiLCJpc3MiOiJzb21lLWlzc3VlciIsIm5vbmNlIjoic29tZS1ub25jZSJ9.sBWi3_4cfGwrmMFZWkCghw4uvCnHN35h9xNX1gkwOtj6Oz_yKqpj7wfO4AqeWsRyrDGnkmIZbVuhAAJqPSi4GlNzN4NU8zh53PGDUpFlpDI1dvqDjIRb9iIEJpRIj34--Sz41H0ooxviIzvUdZFvQlaSzLOqgjR3ddHe2urhbtUuz_DsabP84AWo2DSg0y3ull6DRvk_DvzC6HNN8JwVi08fFvvV9BVq8kjdVeob7gajJkuGSTjsxNZGs5rbBuxBx0MZTQ8boR1fDNdG70GoIb4SsCoBSs7pZxtmGZPHInteY1SilHDDDmpQuE-LvSmvvPN_Cyk1d3eS-IR7hBbCAA"}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "sub": "some-subject"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzb21lLXN1YmplY3QiLCJuYW1lIjoiSm9obiBEb2UiLCJpc3MiOiJzb21lLWlzc3VlciIsIm5vbmNlIjoic29tZS1ub25jZSJ9.sBWi3_4cfGwrmMFZWkCghw4uvCnHN35h9xNX1gkwOtj6Oz_yKqpj7wfO4AqeWsRyrDGnkmIZbVuhAAJqPSi4GlNzN4NU8zh53PGDUpFlpDI1dvqDjIRb9iIEJpRIj34--Sz41H0ooxviIzvUdZFvQlaSzLOqgjR3ddHe2urhbtUuz_DsabP84AWo2DSg0y3ull6DRvk_DvzC6HNN8JwVi08fFvvV9BVq8kjdVeob7gajJkuGSTjsxNZGs5rbBuxBx0MZTQ8boR1fDNdG70GoIb4SsCoBSs7pZxtmGZPHInteY1SilHDDDmpQuE-LvSmvvPN_Cyk1d3eS-IR7hBbCAA",
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer", // takes the issuer from the ID token, since the userinfo one is unreliable.
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
"sub": "some-subject",
|
||||||
|
"name": "Pinny TheSeal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token with id, access and refresh tokens and valid nonce, but userinfo has a different issuer",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "iss": "some-other-issuer", "sub": "some-subject"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: goodIDToken,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer", // takes the issuer from the ID token, since the userinfo one is unreliable.
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
"sub": "some-subject",
|
||||||
|
"name": "Pinny TheSeal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token with id, access and refresh tokens and valid nonce, but no userinfo endpoint from discovery and it's not required",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
requireUserInfo: false,
|
||||||
|
rawClaims: []byte(`{"not_the_userinfo_endpoint": "some-other-endpoint"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: goodIDToken,
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer",
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
"sub": "some-subject",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token with no id token but valid userinfo",
|
||||||
|
tok: testTokenWithoutIDToken,
|
||||||
|
nonce: "",
|
||||||
|
requireIDToken: false,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "iss": "some-other-issuer", "sub": "some-subject"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: "",
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"sub": "some-subject",
|
||||||
|
"name": "Pinny TheSeal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token with neither id token nor userinfo",
|
||||||
|
tok: testTokenWithoutIDToken,
|
||||||
|
nonce: "",
|
||||||
|
requireIDToken: false,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Claims: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token with id, access and refresh tokens, valid nonce, and userinfo subject that doesn't match",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-other-subject", `{"name": "Pinny TheSeal", "sub": "some-other-subject"}`),
|
||||||
|
wantErr: "could not fetch user info claims: userinfo 'sub' claim (some-other-subject) did not match id_token 'sub' claim (some-subject)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "id token not required but is provided, and subjects don't match",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: false,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-other-subject", `{"name": "Pinny TheSeal", "sub": "some-other-subject"}`),
|
||||||
|
wantErr: "could not fetch user info claims: userinfo 'sub' claim (some-other-subject) did not match id_token 'sub' claim (some-subject)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid id token",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": "not-an-id-token"}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-other-subject", `{"name": "Pinny TheSeal", "sub": "some-other-subject"}`),
|
||||||
|
wantErr: "received invalid ID token: oidc: malformed jwt: square/go-jose: compact JWS format must have three parts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid nonce",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": goodIDToken}),
|
||||||
|
nonce: "some-other-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-other-subject", `{"name": "Pinny TheSeal", "sub": "some-other-subject"}`),
|
||||||
|
wantErr: "received ID token with invalid nonce: invalid nonce (expected \"some-other-nonce\", got \"some-nonce\")",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expected to have id token, but doesn't",
|
||||||
|
tok: testTokenWithoutIDToken,
|
||||||
|
nonce: "some-other-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "sub": "some-subject"}`),
|
||||||
|
wantErr: "received response missing ID token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expected to have userinfo, but doesn't",
|
||||||
|
tok: testTokenWithoutIDToken,
|
||||||
|
nonce: "some-other-nonce",
|
||||||
|
requireUserInfo: true,
|
||||||
|
rawClaims: []byte(`{}`),
|
||||||
|
wantErr: "could not fetch user info claims: userinfo endpoint not found, but is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expected to have id token and userinfo, but doesn't have either",
|
||||||
|
tok: testTokenWithoutIDToken,
|
||||||
|
nonce: "some-other-nonce",
|
||||||
|
requireUserInfo: true,
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{}`),
|
||||||
|
wantErr: "received response missing ID token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatched access token hash",
|
||||||
|
tok: testTokenWithoutIDToken,
|
||||||
|
nonce: "some-other-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-subject", `{"name": "Pinny TheSeal", "sub": "some-subject"}`),
|
||||||
|
wantErr: "received response missing ID token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "id token missing subject, skip userinfo check",
|
||||||
|
tok: testTokenWithoutIDToken.WithExtra(map[string]interface{}{"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpc3MiOiJzb21lLWlzc3VlciIsIm5vbmNlIjoic29tZS1ub25jZSJ9.aIhrhikAnQ4Mb1g6RAT08qqflT2LLLi2yj4F2S4zud8nYad4tfEd2ITVJ4Njdjf70ubqyzZ6XxojtC4OqaWbDaQOcd95sd3PW58SYrf4NMvEStFkcMG0HMhJEZLVGnuJQstuq3G9h5Z5bFCkx4mFNo5ho_isBWyHpk-uF14duXXlIDB10SnyZ9dRbcmu-3mMOq0g4oCUPEDiHWkv-Rf70Mk0harL2xvcpxlSMLK4glDfiiki5gl6IReIo4rTVosXAqv3JmjLDeVLtJQRG6F8YcIlDCIfUEUfk0GeYacBVjoDIO570ywVJy1LGvyUuvgXNQUjq2JgzCfb8HWGp7iJdQ"}),
|
||||||
|
nonce: "some-nonce",
|
||||||
|
requireIDToken: true,
|
||||||
|
rawClaims: []byte(`{"userinfo_endpoint": "not-empty"}`),
|
||||||
|
userInfo: forceUserInfoWithClaims("some-other-subject", `{"name": "Pinny TheSeal", "sub": "some-other-subject"}`),
|
||||||
|
wantMergedTokens: &oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Type: "test-token-type",
|
||||||
|
Expiry: metav1.NewTime(expiryTime),
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-initial-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpc3MiOiJzb21lLWlzc3VlciIsIm5vbmNlIjoic29tZS1ub25jZSJ9.aIhrhikAnQ4Mb1g6RAT08qqflT2LLLi2yj4F2S4zud8nYad4tfEd2ITVJ4Njdjf70ubqyzZ6XxojtC4OqaWbDaQOcd95sd3PW58SYrf4NMvEStFkcMG0HMhJEZLVGnuJQstuq3G9h5Z5bFCkx4mFNo5ho_isBWyHpk-uF14duXXlIDB10SnyZ9dRbcmu-3mMOq0g4oCUPEDiHWkv-Rf70Mk0harL2xvcpxlSMLK4glDfiiki5gl6IReIo4rTVosXAqv3JmjLDeVLtJQRG6F8YcIlDCIfUEUfk0GeYacBVjoDIO570ywVJy1LGvyUuvgXNQUjq2JgzCfb8HWGp7iJdQ",
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"iss": "some-issuer",
|
||||||
|
"name": "John Doe",
|
||||||
|
"nonce": "some-nonce",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := ProviderConfig{
|
||||||
|
Name: "test-name",
|
||||||
|
UsernameClaim: "test-username-claim",
|
||||||
|
GroupsClaim: "test-groups-claim",
|
||||||
|
Config: &oauth2.Config{
|
||||||
|
ClientID: "test-client-id",
|
||||||
|
ClientSecret: "test-client-secret",
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "https://example.com",
|
||||||
|
TokenURL: "https://example.com",
|
||||||
|
AuthStyle: oauth2.AuthStyleInParams,
|
||||||
|
},
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
},
|
||||||
|
Provider: &mockProvider{
|
||||||
|
rawClaims: tt.rawClaims,
|
||||||
|
userInfo: tt.userInfo,
|
||||||
|
userInfoErr: tt.userInfoErr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
gotTok, err := p.ValidateTokenAndMergeWithUserInfo(context.Background(), tt.tok, tt.nonce, tt.requireIDToken, tt.requireUserInfo)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.wantErr, err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.wantMergedTokens, gotTok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("ExchangeAuthcodeAndValidateTokens", func(t *testing.T) {
|
t.Run("ExchangeAuthcodeAndValidateTokens", func(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -779,6 +1185,36 @@ func TestProviderConfig(t *testing.T) {
|
|||||||
rawClaims: []byte(`{}`), // user info not supported
|
rawClaims: []byte(`{}`), // user info not supported
|
||||||
wantUserInfoCalled: false,
|
wantUserInfoCalled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "valid but userinfo endpoint could not be found due to parse error",
|
||||||
|
authCode: "valid",
|
||||||
|
returnIDTok: validIDToken,
|
||||||
|
wantToken: oidctypes.Token{
|
||||||
|
AccessToken: &oidctypes.AccessToken{
|
||||||
|
Token: "test-access-token",
|
||||||
|
Expiry: metav1.Time{},
|
||||||
|
},
|
||||||
|
RefreshToken: &oidctypes.RefreshToken{
|
||||||
|
Token: "test-refresh-token",
|
||||||
|
},
|
||||||
|
IDToken: &oidctypes.IDToken{
|
||||||
|
Token: validIDToken,
|
||||||
|
Expiry: metav1.Time{},
|
||||||
|
Claims: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"bat": "baz",
|
||||||
|
"aud": "test-client-id",
|
||||||
|
"iat": 1.606768593e+09,
|
||||||
|
"jti": "test-jti",
|
||||||
|
"nbf": 1.606768593e+09,
|
||||||
|
"sub": "test-user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// cannot be parsed as json, but note that in this case constructing a real provider would have failed
|
||||||
|
rawClaims: []byte(`{`),
|
||||||
|
wantUserInfoCalled: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "valid",
|
name: "valid",
|
||||||
authCode: "valid",
|
authCode: "valid",
|
||||||
@ -808,13 +1244,6 @@ func TestProviderConfig(t *testing.T) {
|
|||||||
rawClaims: []byte(`{}`), // user info not supported
|
rawClaims: []byte(`{}`), // user info not supported
|
||||||
wantUserInfoCalled: false,
|
wantUserInfoCalled: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "user info discovery parse error",
|
|
||||||
authCode: "valid",
|
|
||||||
returnIDTok: validIDToken,
|
|
||||||
rawClaims: []byte(`junk`), // user info discovery fails
|
|
||||||
wantErr: "could not fetch user info claims: could not unmarshal discovery JSON: invalid character 'j' looking for beginning of value",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "user info fetch error",
|
name: "user info fetch error",
|
||||||
authCode: "valid",
|
authCode: "valid",
|
||||||
|
@ -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 oidcclient implements a CLI OIDC login flow.
|
// Package oidcclient implements a CLI OIDC login flow.
|
||||||
@ -435,7 +435,9 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (
|
|||||||
authorizeURL := h.oauth2Config.AuthCodeURL(h.state.String(), *authorizeOptions...)
|
authorizeURL := h.oauth2Config.AuthCodeURL(h.state.String(), *authorizeOptions...)
|
||||||
|
|
||||||
// Don't follow redirects automatically because we want to handle redirects here.
|
// Don't follow redirects automatically because we want to handle redirects here.
|
||||||
|
var sawRedirect bool
|
||||||
h.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
h.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
sawRedirect = true
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -454,8 +456,8 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (
|
|||||||
}
|
}
|
||||||
_ = authRes.Body.Close() // don't need the response body, and okay if it fails to close
|
_ = authRes.Body.Close() // don't need the response body, and okay if it fails to close
|
||||||
|
|
||||||
// A successful authorization always results in a 302.
|
// A successful authorization always results in a redirect (we are flexible on the exact status code).
|
||||||
if authRes.StatusCode != http.StatusFound {
|
if !sawRedirect {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"error getting authorization: expected to be redirected, but response status was %s", authRes.Status)
|
"error getting authorization: expected to be redirected, but response status was %s", authRes.Status)
|
||||||
}
|
}
|
||||||
@ -820,7 +822,7 @@ func (h *handlerState) handleRefresh(ctx context.Context, refreshToken *oidctype
|
|||||||
|
|
||||||
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at least
|
// The spec is not 100% clear about whether an ID token from the refresh flow should include a nonce, and at least
|
||||||
// some providers do not include one, so we skip the nonce validation here (but not other validations).
|
// some providers do not include one, so we skip the nonce validation here (but not other validations).
|
||||||
return upstreamOIDCIdentityProvider.ValidateToken(ctx, refreshed, "")
|
return upstreamOIDCIdentityProvider.ValidateTokenAndMergeWithUserInfo(ctx, refreshed, "", true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Request) (err error) {
|
func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
|
@ -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 oidcclient
|
package oidcclient
|
||||||
@ -19,8 +19,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-logr/stdr"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -408,7 +406,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
|||||||
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||||
mock := mockUpstream(t)
|
mock := mockUpstream(t)
|
||||||
mock.EXPECT().
|
mock.EXPECT().
|
||||||
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
|
ValidateTokenAndMergeWithUserInfo(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce(""), true, false).
|
||||||
Return(&testToken, nil)
|
Return(&testToken, nil)
|
||||||
mock.EXPECT().
|
mock.EXPECT().
|
||||||
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
|
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
|
||||||
@ -455,7 +453,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
|||||||
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||||
mock := mockUpstream(t)
|
mock := mockUpstream(t)
|
||||||
mock.EXPECT().
|
mock.EXPECT().
|
||||||
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
|
ValidateTokenAndMergeWithUserInfo(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce(""), true, false).
|
||||||
Return(nil, fmt.Errorf("some validation error"))
|
Return(nil, fmt.Errorf("some validation error"))
|
||||||
mock.EXPECT().
|
mock.EXPECT().
|
||||||
PerformRefresh(gomock.Any(), "test-refresh-token-returning-invalid-id-token").
|
PerformRefresh(gomock.Any(), "test-refresh-token-returning-invalid-id-token").
|
||||||
@ -902,7 +900,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
|||||||
`/authorize?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&pinniped_idp_name=some-upstream-name&pinniped_idp_type=ldap&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state": some error fetching authorize endpoint`,
|
`/authorize?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&pinniped_idp_name=some-upstream-name&pinniped_idp_type=ldap&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state": some error fetching authorize endpoint`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap login when the OIDC provider authorization endpoint returns something other than a 302 redirect",
|
name: "ldap login when the OIDC provider authorization endpoint returns something other than a redirect",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
opt: func(t *testing.T) Option {
|
opt: func(t *testing.T) Option {
|
||||||
return func(h *handlerState) error {
|
return func(h *handlerState) error {
|
||||||
@ -1217,6 +1215,117 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
|||||||
},
|
},
|
||||||
wantToken: &testToken,
|
wantToken: &testToken,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "successful ldap login with env vars for username and password, http.StatusSeeOther redirect",
|
||||||
|
clientID: "test-client-id",
|
||||||
|
opt: func(t *testing.T) Option {
|
||||||
|
return func(h *handlerState) error {
|
||||||
|
fakeAuthCode := "test-authcode-value"
|
||||||
|
|
||||||
|
h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||||
|
mock := mockUpstream(t)
|
||||||
|
mock.EXPECT().
|
||||||
|
ExchangeAuthcodeAndValidateTokens(
|
||||||
|
gomock.Any(), fakeAuthCode, pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), "http://127.0.0.1:0/callback").
|
||||||
|
Return(&testToken, nil)
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
h.generateState = func() (state.State, error) { return "test-state", nil }
|
||||||
|
h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil }
|
||||||
|
h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil }
|
||||||
|
h.getEnv = func(key string) string {
|
||||||
|
switch key {
|
||||||
|
case "PINNIPED_USERNAME":
|
||||||
|
return "some-upstream-username"
|
||||||
|
case "PINNIPED_PASSWORD":
|
||||||
|
return "some-upstream-password"
|
||||||
|
default:
|
||||||
|
return "" // all other env vars are treated as if they are unset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.promptForValue = func(_ context.Context, promptLabel string) (string, error) {
|
||||||
|
require.FailNow(t, fmt.Sprintf("saw unexpected prompt from the CLI: %q", promptLabel))
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
h.promptForSecret = func(promptLabel string) (string, error) {
|
||||||
|
require.FailNow(t, fmt.Sprintf("saw unexpected prompt from the CLI: %q", promptLabel))
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := &mockSessionCache{t: t, getReturnsToken: nil}
|
||||||
|
cacheKey := SessionCacheKey{
|
||||||
|
Issuer: successServer.URL,
|
||||||
|
ClientID: "test-client-id",
|
||||||
|
Scopes: []string{"test-scope"},
|
||||||
|
RedirectURI: "http://localhost:0/callback",
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys)
|
||||||
|
require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawPutKeys)
|
||||||
|
require.Equal(t, []*oidctypes.Token{&testToken}, cache.sawPutTokens)
|
||||||
|
})
|
||||||
|
require.NoError(t, WithSessionCache(cache)(h))
|
||||||
|
require.NoError(t, WithCLISendingCredentials()(h))
|
||||||
|
require.NoError(t, WithUpstreamIdentityProvider("some-upstream-name", "ldap")(h))
|
||||||
|
|
||||||
|
discoveryRequestWasMade := false
|
||||||
|
authorizeRequestWasMade := false
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.True(t, discoveryRequestWasMade, "should have made an discovery request")
|
||||||
|
require.True(t, authorizeRequestWasMade, "should have made an authorize request")
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, WithClient(&http.Client{
|
||||||
|
Transport: roundtripper.Func(func(req *http.Request) (*http.Response, error) {
|
||||||
|
switch req.URL.Scheme + "://" + req.URL.Host + req.URL.Path {
|
||||||
|
case "http://" + successServer.Listener.Addr().String() + "/.well-known/openid-configuration":
|
||||||
|
discoveryRequestWasMade = true
|
||||||
|
return defaultDiscoveryResponse(req)
|
||||||
|
case "http://" + successServer.Listener.Addr().String() + "/authorize":
|
||||||
|
authorizeRequestWasMade = true
|
||||||
|
require.Equal(t, "some-upstream-username", req.Header.Get("Pinniped-Username"))
|
||||||
|
require.Equal(t, "some-upstream-password", req.Header.Get("Pinniped-Password"))
|
||||||
|
require.Equal(t, url.Values{
|
||||||
|
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
|
||||||
|
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
|
||||||
|
// VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g
|
||||||
|
"code_challenge": []string{"VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"},
|
||||||
|
"code_challenge_method": []string{"S256"},
|
||||||
|
"response_type": []string{"code"},
|
||||||
|
"scope": []string{"test-scope"},
|
||||||
|
"nonce": []string{"test-nonce"},
|
||||||
|
"state": []string{"test-state"},
|
||||||
|
"access_type": []string{"offline"},
|
||||||
|
"client_id": []string{"test-client-id"},
|
||||||
|
"redirect_uri": []string{"http://127.0.0.1:0/callback"},
|
||||||
|
"pinniped_idp_name": []string{"some-upstream-name"},
|
||||||
|
"pinniped_idp_type": []string{"ldap"},
|
||||||
|
}, req.URL.Query())
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusSeeOther,
|
||||||
|
Header: http.Header{"Location": []string{
|
||||||
|
fmt.Sprintf("http://127.0.0.1:0/callback?code=%s&state=test-state", fakeAuthCode),
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
// Note that "/token" requests should not be made. They are mocked by mocking calls to ExchangeAuthcodeAndValidateTokens().
|
||||||
|
require.FailNow(t, fmt.Sprintf("saw unexpected http call from the CLI: %s", req.URL.String()))
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})(h))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
},
|
||||||
|
issuer: successServer.URL,
|
||||||
|
wantLogs: []string{
|
||||||
|
"\"level\"=4 \"msg\"=\"Pinniped: Performing OIDC discovery\" \"issuer\"=\"" + successServer.URL + "\"",
|
||||||
|
"\"level\"=4 \"msg\"=\"Pinniped: Read username from environment variable\" \"name\"=\"PINNIPED_USERNAME\"",
|
||||||
|
"\"level\"=4 \"msg\"=\"Pinniped: Read password from environment variable\" \"name\"=\"PINNIPED_PASSWORD\"",
|
||||||
|
},
|
||||||
|
wantToken: &testToken,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "with requested audience, session cache hit with valid token, but discovery fails",
|
name: "with requested audience, session cache hit with valid token, but discovery fails",
|
||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
@ -1539,7 +1648,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
|||||||
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
h.getProvider = func(config *oauth2.Config, provider *oidc.Provider, client *http.Client) provider.UpstreamOIDCIdentityProviderI {
|
||||||
mock := mockUpstream(t)
|
mock := mockUpstream(t)
|
||||||
mock.EXPECT().
|
mock.EXPECT().
|
||||||
ValidateToken(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce("")).
|
ValidateTokenAndMergeWithUserInfo(gomock.Any(), HasAccessToken(testToken.AccessToken.Token), nonce.Nonce(""), true, false).
|
||||||
Return(&testToken, nil)
|
Return(&testToken, nil)
|
||||||
mock.EXPECT().
|
mock.EXPECT().
|
||||||
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
|
PerformRefresh(gomock.Any(), testToken.RefreshToken.Token).
|
||||||
@ -1571,9 +1680,8 @@ 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.New(t)
|
testLogger := testlogger.NewLegacy(t) //nolint: staticcheck // old test with lots of log statements
|
||||||
klog.SetLogger(testLogger)
|
klog.SetLogger(testLogger.Logger)
|
||||||
stdr.SetVerbosity(debugLogLevel) // set stdr's global log level to debug so the test logger will send output.
|
|
||||||
|
|
||||||
tok, err := Login(tt.issuer, tt.clientID,
|
tok, err := Login(tt.issuer, tt.clientID,
|
||||||
WithContext(context.Background()),
|
WithContext(context.Background()),
|
||||||
@ -1581,7 +1689,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo
|
|||||||
WithScopes([]string{"test-scope"}),
|
WithScopes([]string{"test-scope"}),
|
||||||
WithSkipBrowserOpen(),
|
WithSkipBrowserOpen(),
|
||||||
tt.opt(t),
|
tt.opt(t),
|
||||||
WithLogger(testLogger),
|
WithLogger(testLogger.Logger),
|
||||||
)
|
)
|
||||||
testLogger.Expect(tt.wantLogs)
|
testLogger.Expect(tt.wantLogs)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
|
@ -35,9 +35,9 @@ layout: section
|
|||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h3><a href="{{< param "community_url" >}}">Community Meetings</a></h3>
|
<h3><a href="{{< param "community_url" >}}">Community Meetings</a></h3>
|
||||||
<p>Pinniped Community Meetings are held every first and third Thursday of the month at 9 AM PT / 12 PM ET</p>
|
<p>Pinniped <a href="https://go.pinniped.dev/community/agenda">Community Meetings</a> are on hold until early 2022. Stay tuned for more details on our return. Happy Holidays!</p>
|
||||||
<p>Join our <a href="https://groups.google.com/u/1/g/project-pinniped">Google Group</a> to receive invites to the meeting</p>
|
<p>Join our <a href="https://groups.google.com/u/1/g/project-pinniped">Google Group</a> to receive invites to the meeting</p>
|
||||||
<p>Watch previous community meetings on our <a href="https://www.youtube.com/playlist?list=PL7bmigfV0EqQ8qYn8ornuJnuGvCt0belt">YouTube playlist</a></p>
|
<p>Watch previous community meetings on our <a href="https://go.pinniped.dev/community/youtube">YouTube playlist</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,31 +12,6 @@ layout: section
|
|||||||
<h2>Resources about Pinniped, such as videos, podcasts, and community articles</h2>
|
<h2>Resources about Pinniped, such as videos, podcasts, and community articles</h2>
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
|
|
||||||
<div class="col">
|
|
||||||
<div class="embed-responsive">
|
|
||||||
<iframe class="embed-responsive-item"
|
|
||||||
src="https://www.youtube-nocookie.com/embed/YGeO1jKfgac?start=138"
|
|
||||||
title="YouTube video player"
|
|
||||||
frameborder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowfullscreen>
|
|
||||||
</iframe>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>
|
|
||||||
<a href="https://www.cncf.io/online-programs/cncf-live-webinar-easy-secure-kubernetes-authentication-with-pinniped/"
|
|
||||||
target="_blank">
|
|
||||||
CNCF Live Webinar: Easy, Secure Kubernetes Authentication With Pinniped - August 24, 2021
|
|
||||||
[VIDEO]
|
|
||||||
</a>
|
|
||||||
<a href="https://docs.google.com/presentation/d/1euA62C7SHQpHewPqPaxTvNEKdvyNOdU9MDWh3YN3NvY/edit?usp=sharing"
|
|
||||||
target="_blank">
|
|
||||||
[SLIDES]
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<a href="https://github.com/vmware-tanzu/pinniped">
|
<a href="https://github.com/vmware-tanzu/pinniped">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
@ -49,5 +24,57 @@ layout: section
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="/img/logo.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p class="strong">Community Meetings and Demos</p>
|
||||||
|
<p>We have a YouTube playlist for our Pinniped community meetings and demos, <a href="https://go.pinniped.dev/community/youtube">check it out here</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="embed-responsive">
|
||||||
|
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/OCkTnElNE9M"></iframe>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p><a href="https://www.youtube.com/watch?v=OCkTnElNE9M&feature=emb_logo">KubeCon + CloudNativeCon North America 2021: Everything Wrong with K8s Authentication and How We Worked Around It with Mo Khan & Margo Crawford</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="embed-responsive">
|
||||||
|
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/2fI_XOGEoIU"></iframe>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p><a href="https://www.youtube.com/watch?v=2fI_XOGEoIU&feature=emb_logo">Pinniped: A Unified Framework for User Authentication to Kubernetes Cluster with Mo Khan & Anjali Telang</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="embed-responsive">
|
||||||
|
<iframe class="embed-responsive-item"
|
||||||
|
src="https://www.youtube-nocookie.com/embed/YGeO1jKfgac?start=138"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
<a href="https://www.cncf.io/online-programs/cncf-live-webinar-easy-secure-kubernetes-authentication-with-pinniped/"
|
||||||
|
target="_blank">
|
||||||
|
CNCF Live Webinar: Easy, Secure Kubernetes Authentication With Pinniped - August 24, 2021
|
||||||
|
[VIDEO]
|
||||||
|
</a>
|
||||||
|
<a href="https://docs.google.com/presentation/d/1euA62C7SHQpHewPqPaxTvNEKdvyNOdU9MDWh3YN3NvY/edit?usp=sharing"
|
||||||
|
target="_blank">
|
||||||
|
[SLIDES]
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
[[redirects]]
|
[[redirects]]
|
||||||
from = "/community/agenda"
|
from = "/community/agenda"
|
||||||
to = "https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view"
|
to = "https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ"
|
||||||
status = 302
|
status = 302
|
||||||
force = true
|
force = true
|
||||||
|
|
||||||
|
@ -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 integration
|
package integration
|
||||||
@ -83,7 +83,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(nil)),
|
provider: upstreamldap.New(*providerConfig(nil)),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -95,7 +97,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -104,7 +108,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -113,7 +119,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -125,7 +133,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.Filter = "cn={}"
|
p.UserSearch.Filter = "cn={}"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -136,7 +146,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -147,7 +159,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -156,7 +170,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -165,7 +181,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -174,7 +192,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
password: pinnyPassword,
|
password: pinnyPassword,
|
||||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev", // note that the final answer has case preserved from the entry
|
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev", // note that the final answer has case preserved from the entry
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -187,7 +207,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.UIDAttribute = "givenName"
|
p.UserSearch.UIDAttribute = "givenName"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -199,7 +221,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.UserSearch.UsernameAttribute = "cn"
|
p.UserSearch.UsernameAttribute = "cn"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -220,7 +244,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Base = ""
|
p.GroupSearch.Base = ""
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: nil},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -231,7 +257,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: nil},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -245,7 +273,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||||
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -259,7 +289,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||||
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -270,7 +302,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -281,7 +315,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -292,7 +328,9 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
|||||||
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
||||||
})),
|
})),
|
||||||
wantAuthResponse: &authenticators.Response{
|
wantAuthResponse: &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: nil},
|
||||||
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -671,8 +709,9 @@ func TestSimultaneousLDAPRequestsOnSingleProvider(t *testing.T) {
|
|||||||
assert.NoError(t, result.err)
|
assert.NoError(t, result.err)
|
||||||
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
|
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
|
||||||
assert.Equal(t, &authenticators.Response{
|
assert.Equal(t, &authenticators.Response{
|
||||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||||
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||||
|
ExtraRefreshAttributes: map[string]string{},
|
||||||
}, result.response)
|
}, result.response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
// 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 integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -19,15 +24,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/text/encoding/unicode"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||||
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||||
"go.pinniped.dev/internal/certauthority"
|
"go.pinniped.dev/internal/certauthority"
|
||||||
|
"go.pinniped.dev/internal/crypto/ptls"
|
||||||
"go.pinniped.dev/internal/oidc"
|
"go.pinniped.dev/internal/oidc"
|
||||||
"go.pinniped.dev/internal/psession"
|
"go.pinniped.dev/internal/psession"
|
||||||
"go.pinniped.dev/internal/testutil"
|
"go.pinniped.dev/internal/testutil"
|
||||||
@ -38,24 +46,26 @@ import (
|
|||||||
"go.pinniped.dev/test/testlib/browsertest"
|
"go.pinniped.dev/test/testlib/browsertest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
func TestSupervisorLogin(t *testing.T) {
|
func TestSupervisorLogin(t *testing.T) {
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
maybeSkip func(t *testing.T)
|
maybeSkip func(t *testing.T)
|
||||||
|
createTestUser func(t *testing.T) (string, string)
|
||||||
|
deleteTestUser func(t *testing.T, username string)
|
||||||
|
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||||
createIDP func(t *testing.T) string
|
createIDP func(t *testing.T) string
|
||||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
|
|
||||||
wantDownstreamIDTokenSubjectToMatch string
|
wantDownstreamIDTokenSubjectToMatch string
|
||||||
wantDownstreamIDTokenUsernameToMatch string
|
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||||
wantDownstreamIDTokenGroups []string
|
wantDownstreamIDTokenGroups []string
|
||||||
wantErrorDescription string
|
wantErrorDescription string
|
||||||
wantErrorType string
|
wantErrorType string
|
||||||
|
|
||||||
// We don't necessarily have any way to revoke the user's session on the upstream provider,
|
// Either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||||
// so to cause the upstream refresh to fail we can cheat by manipulating the user's session
|
|
||||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName string)
|
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "oidc with default username and groups claim settings",
|
name: "oidc with default username and groups claim settings",
|
||||||
@ -76,16 +86,14 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
return oidcIDP.Name
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
pinnipedSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer"
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
|
||||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
|
||||||
},
|
},
|
||||||
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "oidc with custom username and groups claim settings",
|
name: "oidc with custom username and groups claim settings",
|
||||||
@ -113,14 +121,46 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
return oidcIDP.Name
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
fositeSessionData := pinnipedSession.Fosite
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
fositeSessionData.Claims.Extra["username"] = "some-incorrect-username"
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
|
||||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
|
||||||
},
|
},
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oidc without refresh token",
|
||||||
|
maybeSkip: func(t *testing.T) {
|
||||||
|
// never need to skip this test
|
||||||
|
},
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||||
|
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||||
|
},
|
||||||
|
Client: idpv1alpha1.OIDCClient{
|
||||||
|
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||||
|
},
|
||||||
|
Claims: idpv1alpha1.OIDCClaims{
|
||||||
|
Username: env.SupervisorUpstreamOIDC.UsernameClaim,
|
||||||
|
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||||
|
},
|
||||||
|
AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{
|
||||||
|
AdditionalScopes: []string{"email"}, // does not ask for offline_access.
|
||||||
|
},
|
||||||
|
}, idpv1alpha1.PhaseReady)
|
||||||
|
return oidcIDP.Name
|
||||||
|
},
|
||||||
|
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||||
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
|
fositeSessionData := pinnipedSession.Fosite
|
||||||
|
fositeSessionData.Claims.Extra["username"] = "some-incorrect-username"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" },
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -144,7 +184,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, idpv1alpha1.PhaseReady)
|
}, idpv1alpha1.PhaseReady)
|
||||||
return oidcIDP.Name
|
return oidcIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamOIDC.Username, // username to present to server during login
|
env.SupervisorUpstreamOIDC.Username, // username to present to server during login
|
||||||
@ -153,7 +193,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||||
@ -162,7 +202,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS",
|
name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS",
|
||||||
@ -212,7 +252,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -221,7 +261,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -235,8 +275,10 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options
|
name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options
|
||||||
@ -286,7 +328,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login
|
||||||
@ -295,7 +337,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -309,7 +351,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$" },
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs,
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -360,7 +402,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -438,7 +480,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -447,7 +489,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -460,8 +502,10 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ldap login still works after deleting and recreating the bind secret",
|
name: "ldap login still works after deleting and recreating the bind secret",
|
||||||
@ -543,7 +587,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -552,7 +596,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||||
@ -565,8 +609,10 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "activedirectory with all default options",
|
name: "activedirectory with all default options",
|
||||||
@ -604,7 +650,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||||
@ -613,7 +659,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -627,8 +673,10 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||||
}, {
|
}, {
|
||||||
name: "activedirectory with custom options",
|
name: "activedirectory with custom options",
|
||||||
maybeSkip: func(t *testing.T) {
|
maybeSkip: func(t *testing.T) {
|
||||||
@ -679,7 +727,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -688,7 +736,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -702,8 +750,10 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "active directory login still works after updating bind secret",
|
name: "active directory login still works after updating bind secret",
|
||||||
@ -759,7 +809,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||||
@ -768,7 +818,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -781,8 +831,10 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "active directory login still works after deleting and recreating bind secret",
|
name: "active directory login still works after deleting and recreating bind secret",
|
||||||
@ -853,7 +905,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
}, time.Minute, 500*time.Millisecond)
|
}, time.Minute, 500*time.Millisecond)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login
|
||||||
@ -862,7 +914,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||||
customSessionData := pinnipedSession.Custom
|
customSessionData := pinnipedSession.Custom
|
||||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||||
@ -875,8 +927,196 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
"&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue,
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "active directory login fails after the user password is changed",
|
||||||
|
maybeSkip: func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||||
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
|
}
|
||||||
|
if env.SupervisorUpstreamActiveDirectory.Host == "" {
|
||||||
|
t.Skip("Active Directory hostname not specified")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
|
map[string]string{
|
||||||
|
v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
|
||||||
|
Host: env.SupervisorUpstreamActiveDirectory.Host,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
|
||||||
|
},
|
||||||
|
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
|
||||||
|
SecretName: secret.Name,
|
||||||
|
},
|
||||||
|
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||||
|
expectedMsg := fmt.Sprintf(
|
||||||
|
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||||
|
env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
secret.Name, secret.ResourceVersion,
|
||||||
|
)
|
||||||
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
|
},
|
||||||
|
createTestUser: func(t *testing.T) (string, string) {
|
||||||
|
return createFreshADTestUser(t, env)
|
||||||
|
},
|
||||||
|
deleteTestUser: func(t *testing.T, username string) {
|
||||||
|
deleteTestADUser(t, env, username)
|
||||||
|
},
|
||||||
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||||
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
|
downstreamAuthorizeURL,
|
||||||
|
testUserName, // username to present to server during login
|
||||||
|
testUserPassword, // password to present to server during login
|
||||||
|
httpClient,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) {
|
||||||
|
changeADTestUserPassword(t, env, username)
|
||||||
|
},
|
||||||
|
// we can't know the subject ahead of time because we created a new user and don't know their uid,
|
||||||
|
// so skip wantDownstreamIDTokenSubjectToMatch
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: []string{}, // none for now.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "active directory login fails after the user is deactivated",
|
||||||
|
maybeSkip: func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||||
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
|
}
|
||||||
|
if env.SupervisorUpstreamActiveDirectory.Host == "" {
|
||||||
|
t.Skip("Active Directory hostname not specified")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
|
map[string]string{
|
||||||
|
v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
|
||||||
|
Host: env.SupervisorUpstreamActiveDirectory.Host,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
|
||||||
|
},
|
||||||
|
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
|
||||||
|
SecretName: secret.Name,
|
||||||
|
},
|
||||||
|
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||||
|
expectedMsg := fmt.Sprintf(
|
||||||
|
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||||
|
env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
secret.Name, secret.ResourceVersion,
|
||||||
|
)
|
||||||
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
|
},
|
||||||
|
createTestUser: func(t *testing.T) (string, string) {
|
||||||
|
return createFreshADTestUser(t, env)
|
||||||
|
},
|
||||||
|
deleteTestUser: func(t *testing.T, username string) {
|
||||||
|
deleteTestADUser(t, env, username)
|
||||||
|
},
|
||||||
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||||
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
|
downstreamAuthorizeURL,
|
||||||
|
testUserName, // username to present to server during login
|
||||||
|
testUserPassword, // password to present to server during login
|
||||||
|
httpClient,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) {
|
||||||
|
deactivateADTestUser(t, env, username)
|
||||||
|
},
|
||||||
|
// we can't know the subject ahead of time because we created a new user and don't know their uid,
|
||||||
|
// so skip wantDownstreamIDTokenSubjectToMatch
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: []string{}, // none for now.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "active directory login fails after the user is locked",
|
||||||
|
maybeSkip: func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
|
||||||
|
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||||
|
}
|
||||||
|
if env.SupervisorUpstreamActiveDirectory.Host == "" {
|
||||||
|
t.Skip("Active Directory hostname not specified")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createIDP: func(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||||
|
map[string]string{
|
||||||
|
v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{
|
||||||
|
Host: env.SupervisorUpstreamActiveDirectory.Host,
|
||||||
|
TLS: &idpv1alpha1.TLSSpec{
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)),
|
||||||
|
},
|
||||||
|
Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{
|
||||||
|
SecretName: secret.Name,
|
||||||
|
},
|
||||||
|
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||||
|
expectedMsg := fmt.Sprintf(
|
||||||
|
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||||
|
env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||||
|
secret.Name, secret.ResourceVersion,
|
||||||
|
)
|
||||||
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
|
return adIDP.Name
|
||||||
|
},
|
||||||
|
createTestUser: func(t *testing.T) (string, string) {
|
||||||
|
return createFreshADTestUser(t, env)
|
||||||
|
},
|
||||||
|
deleteTestUser: func(t *testing.T, username string) {
|
||||||
|
deleteTestADUser(t, env, username)
|
||||||
|
},
|
||||||
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||||
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
|
downstreamAuthorizeURL,
|
||||||
|
testUserName, // username to present to server during login
|
||||||
|
testUserPassword, // password to present to server during login
|
||||||
|
httpClient,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) {
|
||||||
|
lockADTestUser(t, env, username)
|
||||||
|
},
|
||||||
|
// we can't know the subject ahead of time because we created a new user and don't know their uid,
|
||||||
|
// so skip wantDownstreamIDTokenSubjectToMatch
|
||||||
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
|
wantDownstreamIDTokenUsernameToMatch: func(username string) string {
|
||||||
|
return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: []string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "logging in to activedirectory with a deactivated user fails",
|
name: "logging in to activedirectory with a deactivated user fails",
|
||||||
@ -914,7 +1154,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||||
return adIDP.Name
|
return adIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserSAMAccountNameValue, // username to present to server during login
|
env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserSAMAccountNameValue, // username to present to server during login
|
||||||
@ -975,7 +1215,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||||
return ldapIDP.Name
|
return ldapIDP.Name
|
||||||
},
|
},
|
||||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||||
downstreamAuthorizeURL,
|
downstreamAuthorizeURL,
|
||||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||||
@ -984,7 +1224,7 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string) {
|
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, _ string) {
|
||||||
// get the idp, update the config.
|
// get the idp, update the config.
|
||||||
client := testlib.NewSupervisorClientset(t)
|
client := testlib.NewSupervisorClientset(t)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
@ -1007,8 +1247,10 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||||
) + "$",
|
) + "$",
|
||||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||||
|
},
|
||||||
|
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -1020,6 +1262,8 @@ func TestSupervisorLogin(t *testing.T) {
|
|||||||
tt.createIDP,
|
tt.createIDP,
|
||||||
tt.requestAuthorization,
|
tt.requestAuthorization,
|
||||||
tt.breakRefreshSessionData,
|
tt.breakRefreshSessionData,
|
||||||
|
tt.createTestUser,
|
||||||
|
tt.deleteTestUser,
|
||||||
tt.wantDownstreamIDTokenSubjectToMatch,
|
tt.wantDownstreamIDTokenSubjectToMatch,
|
||||||
tt.wantDownstreamIDTokenUsernameToMatch,
|
tt.wantDownstreamIDTokenUsernameToMatch,
|
||||||
tt.wantDownstreamIDTokenGroups,
|
tt.wantDownstreamIDTokenGroups,
|
||||||
@ -1150,10 +1394,15 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes
|
|||||||
func testSupervisorLogin(
|
func testSupervisorLogin(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
createIDP func(t *testing.T) string,
|
createIDP func(t *testing.T) string,
|
||||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
|
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client),
|
||||||
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string),
|
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string),
|
||||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
createTestUser func(t *testing.T) (string, string),
|
||||||
wantErrorDescription string, wantErrorType string,
|
deleteTestUser func(t *testing.T, username string),
|
||||||
|
wantDownstreamIDTokenSubjectToMatch string,
|
||||||
|
wantDownstreamIDTokenUsernameToMatch func(username string) string,
|
||||||
|
wantDownstreamIDTokenGroups []string,
|
||||||
|
wantErrorDescription string,
|
||||||
|
wantErrorType string,
|
||||||
) {
|
) {
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
@ -1241,6 +1490,12 @@ func testSupervisorLogin(
|
|||||||
// Create upstream IDP and wait for it to become ready.
|
// Create upstream IDP and wait for it to become ready.
|
||||||
idpName := createIDP(t)
|
idpName := createIDP(t)
|
||||||
|
|
||||||
|
username, password := "", ""
|
||||||
|
if createTestUser != nil {
|
||||||
|
username, password = createTestUser(t)
|
||||||
|
defer deleteTestUser(t, username)
|
||||||
|
}
|
||||||
|
|
||||||
// Perform OIDC discovery for our downstream.
|
// Perform OIDC discovery for our downstream.
|
||||||
var discovery *coreosoidc.Provider
|
var discovery *coreosoidc.Provider
|
||||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||||
@ -1276,7 +1531,7 @@ func testSupervisorLogin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Perform parameterized auth code acquisition.
|
// Perform parameterized auth code acquisition.
|
||||||
requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, httpClient)
|
requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient)
|
||||||
|
|
||||||
// Expect that our callback handler was invoked.
|
// Expect that our callback handler was invoked.
|
||||||
callback := localCallbackServer.waitForCallback(10 * time.Second)
|
callback := localCallbackServer.waitForCallback(10 * time.Second)
|
||||||
@ -1294,7 +1549,7 @@ func testSupervisorLogin(
|
|||||||
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"}
|
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"}
|
||||||
verifyTokenResponse(t,
|
verifyTokenResponse(t,
|
||||||
tokenResponse, discovery, downstreamOAuth2Config, nonceParam,
|
tokenResponse, discovery, downstreamOAuth2Config, nonceParam,
|
||||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
|
||||||
|
|
||||||
// token exchange on the original token
|
// token exchange on the original token
|
||||||
doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery)
|
doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery)
|
||||||
@ -1308,7 +1563,7 @@ func testSupervisorLogin(
|
|||||||
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"}
|
expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"}
|
||||||
verifyTokenResponse(t,
|
verifyTokenResponse(t,
|
||||||
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
||||||
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch(username), wantDownstreamIDTokenGroups)
|
||||||
|
|
||||||
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
||||||
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
||||||
@ -1333,7 +1588,7 @@ func testSupervisorLogin(
|
|||||||
// Next mutate the part of the session that is used during upstream refresh.
|
// Next mutate the part of the session that is used during upstream refresh.
|
||||||
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
||||||
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
||||||
breakRefreshSessionData(t, pinnipedSession, idpName)
|
breakRefreshSessionData(t, pinnipedSession, idpName, username)
|
||||||
|
|
||||||
// Then save the mutated Secret back to Kubernetes.
|
// Then save the mutated Secret back to Kubernetes.
|
||||||
// There is no update function, so delete and create again at the same name.
|
// There is no update function, so delete and create again at the same name.
|
||||||
@ -1423,7 +1678,7 @@ func verifyTokenResponse(
|
|||||||
require.NotEmpty(t, tokenResponse.RefreshToken)
|
require.NotEmpty(t, tokenResponse.RefreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) {
|
func requestAuthorizationUsingBrowserAuthcodeFlow(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
env := testlib.IntegrationEnv(t)
|
env := testlib.IntegrationEnv(t)
|
||||||
|
|
||||||
@ -1595,3 +1850,153 @@ func expectSecurityHeaders(t *testing.T, response *http.Response, expectFositeTo
|
|||||||
assert.Equal(t, "no-cache", h.Get("Pragma"))
|
assert.Equal(t, "no-cache", h.Get("Pragma"))
|
||||||
assert.Equal(t, "0", h.Get("Expires"))
|
assert.Equal(t, "0", h.Get("Expires"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a fresh test user in AD to use for this test.
|
||||||
|
func createFreshADTestUser(t *testing.T, env *testlib.TestEnv) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
// dial tls
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testUserName := "user-" + createRandomHexString(t, 7) // sAMAccountNames are limited to 20 characters, so this is as long as we can make it.
|
||||||
|
// create
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
a := ldap.NewAddRequest(userDN, []ldap.Control{})
|
||||||
|
a.Attribute("objectClass", []string{"top", "person", "organizationalPerson", "user"})
|
||||||
|
a.Attribute("userPrincipalName", []string{fmt.Sprintf("%s@%s", testUserName, env.SupervisorUpstreamActiveDirectory.Domain)})
|
||||||
|
a.Attribute("sAMAccountName", []string{testUserName})
|
||||||
|
|
||||||
|
err = conn.Add(a)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// modify password and enable account
|
||||||
|
testUserPassword := createRandomASCIIString(t, 20)
|
||||||
|
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||||
|
encodedTestUserPassword, err := enc.String("\"" + testUserPassword + "\"")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||||
|
m.Replace("unicodePwd", []string{encodedTestUserPassword})
|
||||||
|
m.Replace("userAccountControl", []string{"512"})
|
||||||
|
err = conn.Modify(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
return testUserName, testUserPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// deactivate the test user.
|
||||||
|
func deactivateADTestUser(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||||
|
m.Replace("userAccountControl", []string{"514"}) // normal user, account disabled
|
||||||
|
err = conn.Modify(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock the test user's account by entering the wrong password a bunch of times.
|
||||||
|
func lockADTestUser(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
|
||||||
|
// our password policy allows 20 wrong attempts before locking the account, so do 21.
|
||||||
|
// these wrong password attempts could go to different domain controllers, but account
|
||||||
|
// lockout changes are urgently replicated, meaning that the domain controllers will be
|
||||||
|
// synced asap rather than in the usual 15 second interval.
|
||||||
|
// See https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/cc961787(v=technet.10)#urgent-replication-of-account-lockout-changes
|
||||||
|
for i := 0; i <= 21; i++ {
|
||||||
|
err := conn.Bind(userDN, "not-the-right-password-"+fmt.Sprint(i))
|
||||||
|
require.Error(t, err) // this should be an error
|
||||||
|
}
|
||||||
|
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the user's password to a new one.
|
||||||
|
func changeADTestUserPassword(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newTestUserPassword := createRandomASCIIString(t, 20)
|
||||||
|
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||||
|
encodedTestUserPassword, err := enc.String(`"` + newTestUserPassword + `"`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
m := ldap.NewModifyRequest(userDN, []ldap.Control{})
|
||||||
|
m.Replace("unicodePwd", []string{encodedTestUserPassword})
|
||||||
|
err = conn.Modify(m)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||||
|
// don't bother to return the new password... we won't be using it, just checking that it's changed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the test user created for this test.
|
||||||
|
func deleteTestADUser(t *testing.T, env *testlib.TestEnv, testUserName string) {
|
||||||
|
t.Helper()
|
||||||
|
conn := dialTLS(t, env)
|
||||||
|
// bind
|
||||||
|
err := conn.Bind(env.SupervisorUpstreamActiveDirectory.BindUsername, env.SupervisorUpstreamActiveDirectory.BindPassword)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
userDN := fmt.Sprintf("CN=%s,OU=test-users,%s", testUserName, env.SupervisorUpstreamActiveDirectory.UserSearchBase)
|
||||||
|
d := ldap.NewDelRequest(userDN, []ldap.Control{})
|
||||||
|
err = conn.Del(d)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialTLS(t *testing.T, env *testlib.TestEnv) *ldap.Conn {
|
||||||
|
t.Helper()
|
||||||
|
// dial tls
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
success := rootCAs.AppendCertsFromPEM([]byte(env.SupervisorUpstreamActiveDirectory.CABundle))
|
||||||
|
require.True(t, success)
|
||||||
|
tlsConfig := ptls.DefaultLDAP(rootCAs)
|
||||||
|
dialer := &tls.Dialer{NetDialer: &net.Dialer{Timeout: time.Minute}, Config: tlsConfig}
|
||||||
|
c, err := dialer.DialContext(context.Background(), "tcp", env.SupervisorUpstreamActiveDirectory.Host)
|
||||||
|
require.NoError(t, err)
|
||||||
|
conn := ldap.NewConn(c, true)
|
||||||
|
conn.Start()
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRandomHexString(t *testing.T, length int) string {
|
||||||
|
t.Helper()
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
_, err := rand.Read(bytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
randomString := hex.EncodeToString(bytes)
|
||||||
|
return randomString
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRandomASCIIString(t *testing.T, length int) string {
|
||||||
|
result := ""
|
||||||
|
for {
|
||||||
|
if len(result) >= length {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(127)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
n := num.Int64()
|
||||||
|
// Make sure that the number/byte/letter is inside
|
||||||
|
// the range of printable ASCII characters (excluding space and DEL)
|
||||||
|
if n > 32 && n < 127 {
|
||||||
|
result += string(rune(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 testlib
|
package testlib
|
||||||
@ -84,6 +84,7 @@ type TestOIDCUpstream struct {
|
|||||||
|
|
||||||
type TestLDAPUpstream struct {
|
type TestLDAPUpstream struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
StartTLSOnlyHost string `json:"startTLSOnlyHost"`
|
StartTLSOnlyHost string `json:"startTLSOnlyHost"`
|
||||||
CABundle string `json:"caBundle"`
|
CABundle string `json:"caBundle"`
|
||||||
BindUsername string `json:"bindUsername"`
|
BindUsername string `json:"bindUsername"`
|
||||||
@ -279,6 +280,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
|
|||||||
|
|
||||||
result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{
|
result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{
|
||||||
Host: wantEnv("PINNIPED_TEST_AD_HOST", ""),
|
Host: wantEnv("PINNIPED_TEST_AD_HOST", ""),
|
||||||
|
Domain: wantEnv("PINNIPED_TEST_AD_DOMAIN", ""),
|
||||||
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")),
|
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")),
|
||||||
BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""),
|
BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""),
|
||||||
BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""),
|
BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""),
|
||||||
|
Loading…
Reference in New Issue
Block a user