Merge branch 'main' into upstream_refresh_revocation_during_gc
This commit is contained in:
commit
de79f15068
57
.github/workflows/codeql-analysis.yml
vendored
Normal file
57
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, release* ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main, release* ]
|
||||
schedule:
|
||||
- cron: '39 13 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
@ -18,8 +18,8 @@ The near-term and mid-term roadmap for the work planned for the project [maintai
|
||||
|
||||
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,
|
||||
occurring every first and third Thursday of the month at 9 AM PT / 12 PM PT.
|
||||
Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09)
|
||||
occurring every first and third 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
|
||||
to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view).
|
||||
Join our [Google Group](https://groups.google.com/g/project-pinniped) to receive invites to this meeting.
|
||||
|
12
Dockerfile
12
Dockerfile
@ -3,7 +3,7 @@
|
||||
# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.17.2 as build-env
|
||||
FROM golang:1.17.3 as build-env
|
||||
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
@ -17,14 +17,14 @@ RUN \
|
||||
--mount=type=cache,target=/cache/gomodcache \
|
||||
mkdir out && \
|
||||
export GOCACHE=/cache/gocache GOMODCACHE=/cache/gomodcache CGO_ENABLED=0 GOOS=linux GOARCH=amd64 && \
|
||||
go build -v -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-concierge-kube-cert-agent ./cmd/pinniped-concierge-kube-cert-agent/main.go && \
|
||||
go build -v -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-server ./cmd/pinniped-server/main.go && \
|
||||
go build -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-concierge-kube-cert-agent ./cmd/pinniped-concierge-kube-cert-agent/... && \
|
||||
go build -v -trimpath -ldflags "$(hack/get-ldflags.sh) -w -s" -o /usr/local/bin/pinniped-server ./cmd/pinniped-server/... && \
|
||||
ln -s /usr/local/bin/pinniped-server /usr/local/bin/pinniped-concierge && \
|
||||
ln -s /usr/local/bin/pinniped-server /usr/local/bin/pinniped-supervisor && \
|
||||
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.
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:07869abb445859465749913267a8c7b3b02dc4236fbc896e29ae859e4b360851
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:bca3c203cdb36f5914ab8568e4c25165643ea9b711b41a8a58b42c80a51ed609
|
||||
|
||||
# Copy the server binary from the build-env stage.
|
||||
COPY --from=build-env /usr/local/bin /usr/local/bin
|
||||
@ -33,7 +33,9 @@ COPY --from=build-env /usr/local/bin /usr/local/bin
|
||||
EXPOSE 8080 8443
|
||||
|
||||
# Run as non-root for security posture
|
||||
USER 1001:1001
|
||||
# Use the same non-root user as https://github.com/GoogleContainerTools/distroless/blob/fc3c4eaceb0518900f886aae90407c43be0a42d9/base/base.bzl#L9
|
||||
# This is a workaround for https://github.com/GoogleContainerTools/distroless/issues/718
|
||||
USER 65532:65532
|
||||
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/pinniped-server"]
|
||||
|
@ -34,8 +34,8 @@ building and testing the code, submitting PRs, and other contributor topics.
|
||||
|
||||
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,
|
||||
occurring every first and third Thursday of the month at 9 AM PT / 12 PM PT.
|
||||
Use [this Zoom Link](https://vmware.zoom.us/j/93798188973?pwd=T3pIMWxReEQvcWljNm1admRoZTFSZz09)
|
||||
occurring every first and third 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
|
||||
to [the notes document](https://hackmd.io/rd_kVJhjQfOvfAWzK8A3tQ?view).
|
||||
Join our [Google Group](https://groups.google.com/g/project-pinniped) to receive invites to this meeting.
|
||||
|
@ -36,7 +36,7 @@ The following table includes the current roadmap for Pinniped. If you have any q
|
||||
Last Updated: Sept 2021
|
||||
|Theme|Description|Timeline|
|
||||
|--|--|--|
|
||||
|Improving Security Posture|Supervisor token refresh fails when the upstream refresh token no longer works for OIDC |Oct 2021|
|
||||
|Improving Security Posture|Supervisor token refresh fails when the upstream refresh token no longer works for OIDC |Nov 2021|
|
||||
|Improving Security Posture|Supervisor token refresh fails when the upstream user is in an invalid state for LDAP/AD |Nov 2021|
|
||||
|Improving Security Posture|Set stricter default TLS versions and Ciphers |Nov 2021|
|
||||
|Improving Security Posture|Support FIPS compliant Boring crypto libraries |Dec 2021|
|
||||
|
@ -97,6 +97,7 @@ type getKubeconfigParams struct {
|
||||
generatedNameSuffix string
|
||||
credentialCachePath string
|
||||
credentialCachePathSet bool
|
||||
installHint string
|
||||
}
|
||||
|
||||
func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
@ -147,6 +148,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command {
|
||||
f.StringVarP(&flags.outputPath, "output", "o", "", "Output file path (default: stdout)")
|
||||
f.StringVar(&flags.generatedNameSuffix, "generated-name-suffix", "-pinniped", "Suffix to append to generated cluster, context, user kubeconfig entries")
|
||||
f.StringVar(&flags.credentialCachePath, "credential-cache", "", "Path to cluster-specific credentials cache")
|
||||
f.StringVar(&flags.installHint, "install-hint", "The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli for more details", "This text is shown to the user when the pinniped CLI is not installed.")
|
||||
mustMarkHidden(cmd, "oidc-debug-session-cache")
|
||||
|
||||
// --oidc-skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case.
|
||||
@ -259,6 +261,7 @@ func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdap
|
||||
ProvideClusterInfo: true,
|
||||
}
|
||||
|
||||
execConfig.InstallHint = flags.installHint
|
||||
var err error
|
||||
execConfig.Command, err = deps.getPathToSelf()
|
||||
if err != nil {
|
||||
|
@ -133,6 +133,7 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
--credential-cache string Path to cluster-specific credentials cache
|
||||
--generated-name-suffix string Suffix to append to generated cluster, context, user kubeconfig entries (default "-pinniped")
|
||||
-h, --help help for kubeconfig
|
||||
--install-hint string This text is shown to the user when the pinniped CLI is not installed. (default "The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli for more details")
|
||||
--kubeconfig string Path to kubeconfig file
|
||||
--kubeconfig-context string Kubeconfig context name (default: current active context)
|
||||
--no-concierge Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly
|
||||
@ -1326,6 +1327,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --token=test-token
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`)
|
||||
},
|
||||
@ -1389,6 +1392,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --token-env=TEST_TOKEN
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`)
|
||||
},
|
||||
@ -1457,6 +1462,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -1541,6 +1548,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||
@ -1652,6 +1661,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()),
|
||||
@ -1759,6 +1770,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -1836,6 +1849,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-type=ldap
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -1913,6 +1928,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-type=oidc
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -1986,6 +2003,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2057,6 +2076,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2135,6 +2156,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --request-audience=test-audience
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2211,6 +2234,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-flow=foobar
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2292,6 +2317,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-flow=foobar
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2348,6 +2375,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-type=ldap
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2407,6 +2436,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-type=ldap
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2466,6 +2497,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-type=ldap
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2526,6 +2559,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-type=ldap
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2587,6 +2622,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-flow=foobar
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2646,6 +2683,8 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-flow=cli_password
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
@ -2704,12 +2743,77 @@ func TestGetKubeconfig(t *testing.T) {
|
||||
- --upstream-identity-provider-flow=cli_password
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli
|
||||
for more details
|
||||
provideClusterInfo: true
|
||||
`,
|
||||
issuerURL,
|
||||
base64.StdEncoding.EncodeToString([]byte(issuerCABundle)))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user specified message for install-hint flag",
|
||||
args: func(issuerCABundle string, issuerURL string) []string {
|
||||
return []string{
|
||||
"--kubeconfig", "./testdata/kubeconfig.yaml",
|
||||
"--install-hint", "Test installHint message",
|
||||
"--static-token", "test-token",
|
||||
"--skip-validation",
|
||||
}
|
||||
},
|
||||
conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object {
|
||||
return []runtime.Object{
|
||||
credentialIssuer(),
|
||||
&conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}},
|
||||
}
|
||||
},
|
||||
wantLogs: func(issuerCABundle string, issuerURL string) []string {
|
||||
return []string{
|
||||
`"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`,
|
||||
`"level"=0 "msg"="discovered Concierge operating in TokenCredentialRequest API mode"`,
|
||||
`"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://fake-server-url-value"`,
|
||||
`"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`,
|
||||
`"level"=0 "msg"="discovered WebhookAuthenticator" "name"="test-authenticator"`,
|
||||
}
|
||||
},
|
||||
wantStdout: func(issuerCABundle string, issuerURL string) string {
|
||||
return here.Doc(`
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
server: https://fake-server-url-value
|
||||
name: kind-cluster-pinniped
|
||||
contexts:
|
||||
- context:
|
||||
cluster: kind-cluster-pinniped
|
||||
user: kind-user-pinniped
|
||||
name: kind-context-pinniped
|
||||
current-context: kind-context-pinniped
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: kind-user-pinniped
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
args:
|
||||
- login
|
||||
- static
|
||||
- --enable-concierge
|
||||
- --concierge-api-group-suffix=pinniped.dev
|
||||
- --concierge-authenticator-name=test-authenticator
|
||||
- --concierge-authenticator-type=webhook
|
||||
- --concierge-endpoint=https://fake-server-url-value
|
||||
- --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==
|
||||
- --token=test-token
|
||||
command: '.../path/to/pinniped'
|
||||
env: []
|
||||
installHint: Test installHint message
|
||||
provideClusterInfo: true
|
||||
`)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
@ -55,8 +55,8 @@ api_serving_certificate_renew_before_seconds: 2160000
|
||||
#! information), trace (timing information), all (kitchen sink).
|
||||
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
|
||||
|
||||
run_as_user: 1001 #! run_as_user specifies the user ID that will own the process
|
||||
run_as_group: 1001 #! run_as_group specifies the group ID that will own the process
|
||||
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
|
||||
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice
|
||||
|
||||
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
|
||||
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
|
||||
|
@ -15,5 +15,5 @@ image_tag: latest
|
||||
#! Optional.
|
||||
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}
|
||||
|
||||
run_as_user: 1001 #! run_as_user specifies the user ID that will own the process
|
||||
run_as_group: 1001 #! run_as_group specifies the group ID that will own the process
|
||||
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
|
||||
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice
|
||||
|
@ -57,8 +57,8 @@ service_loadbalancer_ip: #! e.g. 1.2.3.4
|
||||
#! information), trace (timing information), all (kitchen sink).
|
||||
log_level: #! By default, when this value is left unset, only warnings and errors are printed. There is no way to suppress warning and error logs.
|
||||
|
||||
run_as_user: 1001 #! run_as_user specifies the user ID that will own the process
|
||||
run_as_group: 1001 #! run_as_group specifies the group ID that will own the process
|
||||
run_as_user: 65532 #! run_as_user specifies the user ID that will own the process, see the Dockerfile for the reasoning behind this choice
|
||||
run_as_group: 65532 #! run_as_group specifies the group ID that will own the process, see the Dockerfile for the reasoning behind this choice
|
||||
|
||||
#! Specify the API group suffix for all Pinniped API groups. By default, this is set to
|
||||
#! pinniped.dev, so Pinniped API groups will look like foo.pinniped.dev,
|
||||
|
@ -92,6 +92,9 @@ while (("$#")); do
|
||||
;;
|
||||
-*)
|
||||
log_error "Unsupported flag $1" >&2
|
||||
if [[ "$1" == *"active-directory"* ]]; then
|
||||
log_error "Did you mean --get-active-directory-vars?"
|
||||
fi
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
|
@ -7,7 +7,7 @@ package authenticators
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
// This interface is similar to the k8s token authenticator, but works with username/passwords instead
|
||||
@ -31,5 +31,10 @@ import (
|
||||
// See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator
|
||||
// interface, as well as the Response type.
|
||||
type UserAuthenticator interface {
|
||||
AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error)
|
||||
AuthenticateUser(ctx context.Context, username, password string) (*Response, bool, error)
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
User user.Info
|
||||
DN string
|
||||
}
|
||||
|
@ -346,14 +346,22 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
|
||||
"providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt",
|
||||
"oidc": {
|
||||
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬"
|
||||
},
|
||||
"ldap": {
|
||||
"userDN": "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾"
|
||||
},
|
||||
"activedirectory": {
|
||||
"userDN": "|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞"
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestedAudience": [
|
||||
"6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾"
|
||||
"ŚB碠k9"
|
||||
],
|
||||
"grantedAudience": [
|
||||
"|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞"
|
||||
"ʘ赱",
|
||||
"ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔",
|
||||
"墀jMʥ"
|
||||
]
|
||||
},
|
||||
"version": "2"
|
||||
|
@ -15,9 +15,9 @@ import (
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
|
||||
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
|
||||
"go.pinniped.dev/internal/authenticators"
|
||||
"go.pinniped.dev/internal/httputil/httperr"
|
||||
"go.pinniped.dev/internal/httputil/securityheader"
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
@ -112,6 +112,7 @@ func handleAuthRequestForLDAPUpstream(
|
||||
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
|
||||
username = authenticateResponse.User.GetName()
|
||||
groups := authenticateResponse.User.GetGroups()
|
||||
dn := authenticateResponse.DN
|
||||
|
||||
customSessionData := &psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstream.GetResourceUID(),
|
||||
@ -119,6 +120,17 @@ func handleAuthRequestForLDAPUpstream(
|
||||
ProviderType: idpType,
|
||||
}
|
||||
|
||||
if idpType == psession.ProviderTypeLDAP {
|
||||
customSessionData.LDAP = &psession.LDAPSessionData{
|
||||
UserDN: dn,
|
||||
}
|
||||
}
|
||||
if idpType == psession.ProviderTypeActiveDirectory {
|
||||
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
|
||||
UserDN: dn,
|
||||
}
|
||||
}
|
||||
|
||||
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
|
||||
oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
|
||||
}
|
||||
@ -470,10 +482,7 @@ func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken
|
||||
return nil
|
||||
}
|
||||
|
||||
func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticator.Response) string {
|
||||
func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string {
|
||||
ldapURL := *ldapUpstream.GetURL()
|
||||
q := ldapURL.Query()
|
||||
q.Set(oidc.IDTokenSubjectClaim, authenticateResponse.User.GetUID())
|
||||
ldapURL.RawQuery = q.Encode()
|
||||
return ldapURL.String()
|
||||
return downstreamsession.DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL)
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ import (
|
||||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"go.pinniped.dev/internal/authenticators"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
"go.pinniped.dev/internal/oidc/csrftoken"
|
||||
@ -267,22 +267,24 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
|
||||
happyLDAPPassword := "some-ldap-password" //nolint:gosec
|
||||
happyLDAPUID := "some-ldap-uid"
|
||||
happyLDAPUserDN := "cn=foo,dn=bar"
|
||||
happyLDAPGroups := []string{"group1", "group2", "group3"}
|
||||
|
||||
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||
if username == "" || password == "" {
|
||||
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
|
||||
}
|
||||
if username == happyLDAPUsername && password == happyLDAPPassword {
|
||||
return &authenticator.Response{
|
||||
return &authenticators.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: happyLDAPUsernameFromAuthenticator,
|
||||
UID: happyLDAPUID,
|
||||
Groups: happyLDAPGroups,
|
||||
},
|
||||
DN: happyLDAPUserDN,
|
||||
}, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
@ -305,7 +307,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: ldapUpstreamName,
|
||||
ResourceUID: ldapUpstreamResourceUID,
|
||||
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||
return nil, false, fmt.Errorf("some ldap upstream auth error")
|
||||
},
|
||||
}
|
||||
@ -438,6 +440,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
ProviderName: activeDirectoryUpstreamName,
|
||||
ProviderType: psession.ProviderTypeActiveDirectory,
|
||||
OIDC: nil,
|
||||
LDAP: nil,
|
||||
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||
UserDN: happyLDAPUserDN,
|
||||
},
|
||||
}
|
||||
|
||||
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
|
||||
@ -445,6 +451,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: psession.ProviderTypeLDAP,
|
||||
OIDC: nil,
|
||||
LDAP: &psession.LDAPSessionData{
|
||||
UserDN: happyLDAPUserDN,
|
||||
},
|
||||
ActiveDirectory: nil,
|
||||
}
|
||||
|
||||
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{
|
||||
|
@ -169,6 +169,13 @@ func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
|
||||
return valueAsString, nil
|
||||
}
|
||||
|
||||
func DownstreamLDAPSubject(uid string, ldapURL url.URL) string {
|
||||
q := ldapURL.Query()
|
||||
q.Set(oidc.IDTokenSubjectClaim, uid)
|
||||
ldapURL.RawQuery = q.Encode()
|
||||
return ldapURL.String()
|
||||
}
|
||||
|
||||
func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string) string {
|
||||
return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidc.IDTokenSubjectClaim, url.QueryEscape(upstreamSubject))
|
||||
}
|
||||
|
@ -91,6 +91,9 @@ type UpstreamLDAPIdentityProviderI interface {
|
||||
|
||||
// UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider.
|
||||
authenticators.UserAuthenticator
|
||||
|
||||
// PerformRefresh performs a refresh against the upstream LDAP identity provider
|
||||
PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error
|
||||
}
|
||||
|
||||
type DynamicUpstreamIDPProvider interface {
|
||||
|
@ -75,6 +75,12 @@ func NewHandler(
|
||||
|
||||
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||
session := accessRequest.GetSession().(*psession.PinnipedSession)
|
||||
downstreamUsername, err := getDownstreamUsernameFromPinnipedSession(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
downstreamSubject := session.Fosite.Claims.Subject
|
||||
|
||||
customSessionData := session.Custom
|
||||
if customSessionData == nil {
|
||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
@ -89,14 +95,12 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
|
||||
case psession.ProviderTypeOIDC:
|
||||
return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
|
||||
case psession.ProviderTypeLDAP:
|
||||
// upstream refresh not yet implemented for LDAP, so do nothing
|
||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
||||
case psession.ProviderTypeActiveDirectory:
|
||||
// upstream refresh not yet implemented for AD, so do nothing
|
||||
return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
|
||||
default:
|
||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error {
|
||||
@ -114,9 +118,9 @@ func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
||||
|
||||
refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken)
|
||||
if err != nil {
|
||||
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||
"Upstream refresh failed using provider %q of type %q.",
|
||||
s.ProviderName, s.ProviderType).WithWrap(err))
|
||||
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||
"Upstream refresh failed.",
|
||||
).WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||
}
|
||||
|
||||
// Upstream refresh may or may not return a new ID token. From the spec:
|
||||
@ -129,8 +133,7 @@ func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, pro
|
||||
_, err = p.ValidateToken(ctx, refreshedTokens, "")
|
||||
if err != nil {
|
||||
return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
|
||||
"Upstream refresh returned an invalid ID token using provider %q of type %q.",
|
||||
s.ProviderName, s.ProviderType).WithWrap(err))
|
||||
"Upstream refresh returned an invalid ID token.").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",
|
||||
@ -163,5 +166,72 @@ func findOIDCProviderByNameAndValidateUID(
|
||||
}
|
||||
}
|
||||
return nil, errorsx.WithStack(errUpstreamRefreshError.
|
||||
WithHintf("Provider %q of type %q from upstream session data was not found.", 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 {
|
||||
// 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 != ""
|
||||
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
|
||||
if !(validLDAP || validAD) {
|
||||
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
|
||||
// get ldap/ad provider out of cache
|
||||
p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// run PerformRefresh
|
||||
err = p.PerformRefresh(ctx, dn, username, subject)
|
||||
if err != nil {
|
||||
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findLDAPProviderByNameAndValidateUID(
|
||||
s *psession.CustomSessionData,
|
||||
providerCache oidc.UpstreamIdentityProvidersLister,
|
||||
) (provider.UpstreamLDAPIdentityProviderI, string, error) {
|
||||
var providers []provider.UpstreamLDAPIdentityProviderI
|
||||
var dn string
|
||||
if s.ProviderType == psession.ProviderTypeLDAP {
|
||||
providers = providerCache.GetLDAPIdentityProviders()
|
||||
dn = s.LDAP.UserDN
|
||||
} else if s.ProviderType == psession.ProviderTypeActiveDirectory {
|
||||
providers = providerCache.GetActiveDirectoryIdentityProviders()
|
||||
dn = s.ActiveDirectory.UserDN
|
||||
}
|
||||
|
||||
for _, p := range providers {
|
||||
if p.GetName() == s.ProviderName {
|
||||
if p.GetResourceUID() != s.ProviderUID {
|
||||
return nil, "", errorsx.WithStack(errUpstreamRefreshError.WithHint(
|
||||
"Provider from upstream session data has changed its resource UID since authentication.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||
}
|
||||
return p, dn, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", errorsx.WithStack(errUpstreamRefreshError.
|
||||
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
|
||||
}
|
||||
|
||||
func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession) (string, error) {
|
||||
extra := session.Fosite.Claims.Extra
|
||||
if extra == nil {
|
||||
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
downstreamUsernameInterface := extra["username"]
|
||||
if downstreamUsernameInterface == nil {
|
||||
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
downstreamUsername, ok := downstreamUsernameInterface.(string)
|
||||
if !ok || len(downstreamUsername) == 0 {
|
||||
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
|
||||
}
|
||||
return downstreamUsername, nil
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ type tokenEndpointResponseExpectedValues struct {
|
||||
wantErrorResponseBody string
|
||||
wantRequestedScopes []string
|
||||
wantGrantedScopes []string
|
||||
wantUpstreamOIDCRefreshCall *expectedUpstreamRefresh
|
||||
wantUpstreamRefreshCall *expectedUpstreamRefresh
|
||||
wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens
|
||||
wantCustomSessionDataStored *psession.CustomSessionData
|
||||
}
|
||||
@ -879,8 +879,20 @@ func TestRefreshGrant(t *testing.T) {
|
||||
oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token"
|
||||
oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token"
|
||||
oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token"
|
||||
|
||||
ldapUpstreamName = "some-ldap-idp"
|
||||
ldapUpstreamResourceUID = "ldap-resource-uid"
|
||||
ldapUpstreamType = "ldap"
|
||||
ldapUpstreamDN = "some-ldap-user-dn"
|
||||
|
||||
activeDirectoryUpstreamName = "some-ad-idp"
|
||||
activeDirectoryUpstreamResourceUID = "ad-resource-uid"
|
||||
activeDirectoryUpstreamType = "activedirectory"
|
||||
activeDirectoryUpstreamDN = "some-ad-user-dn"
|
||||
)
|
||||
|
||||
ldapUpstreamURL, _ := url.Parse("some-url")
|
||||
|
||||
// The below values are funcs so every test can have its own copy of the objects, to avoid data races
|
||||
// in these parallel tests.
|
||||
|
||||
@ -907,7 +919,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
return sessionData
|
||||
}
|
||||
|
||||
happyUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
||||
happyOIDCUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
||||
return &expectedUpstreamRefresh{
|
||||
performedByUpstreamName: oidcUpstreamName,
|
||||
args: &oidctestutil.PerformRefreshArgs{
|
||||
@ -917,6 +929,30 @@ func TestRefreshGrant(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
happyLDAPUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
||||
return &expectedUpstreamRefresh{
|
||||
performedByUpstreamName: ldapUpstreamName,
|
||||
args: &oidctestutil.PerformRefreshArgs{
|
||||
Ctx: nil,
|
||||
DN: ldapUpstreamDN,
|
||||
ExpectedSubject: goodSubject,
|
||||
ExpectedUsername: goodUsername,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
happyActiveDirectoryUpstreamRefreshCall := func() *expectedUpstreamRefresh {
|
||||
return &expectedUpstreamRefresh{
|
||||
performedByUpstreamName: activeDirectoryUpstreamName,
|
||||
args: &oidctestutil.PerformRefreshArgs{
|
||||
Ctx: nil,
|
||||
DN: activeDirectoryUpstreamDN,
|
||||
ExpectedSubject: goodSubject,
|
||||
ExpectedUsername: goodUsername,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token) *expectedUpstreamValidateTokens {
|
||||
return &expectedUpstreamValidateTokens{
|
||||
performedByUpstreamName: oidcUpstreamName,
|
||||
@ -944,7 +980,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
// same as the same values as the authcode exchange case.
|
||||
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
||||
// Should always try to perform an upstream refresh.
|
||||
want.wantUpstreamOIDCRefreshCall = happyUpstreamRefreshCall()
|
||||
want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall()
|
||||
// Should only try to ValidateToken when there was an id token returned by the upstream refresh.
|
||||
if expectToValidateToken != nil {
|
||||
want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken)
|
||||
@ -952,6 +988,18 @@ func TestRefreshGrant(t *testing.T) {
|
||||
return want
|
||||
}
|
||||
|
||||
happyRefreshTokenResponseForLDAP := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues {
|
||||
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
||||
want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall()
|
||||
return want
|
||||
}
|
||||
|
||||
happyRefreshTokenResponseForActiveDirectory := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues {
|
||||
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
|
||||
want.wantUpstreamRefreshCall = happyActiveDirectoryUpstreamRefreshCall()
|
||||
return want
|
||||
}
|
||||
|
||||
refreshedUpstreamTokensWithRefreshTokenWithoutIDToken := func() *oauth2.Token {
|
||||
return &oauth2.Token{
|
||||
AccessToken: "fake-refreshed-access-token",
|
||||
@ -972,11 +1020,28 @@ func TestRefreshGrant(t *testing.T) {
|
||||
return tokens
|
||||
}
|
||||
|
||||
happyActiveDirectoryCustomSessionData := &psession.CustomSessionData{
|
||||
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||
ProviderName: activeDirectoryUpstreamName,
|
||||
ProviderType: activeDirectoryUpstreamType,
|
||||
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||
UserDN: activeDirectoryUpstreamDN,
|
||||
},
|
||||
}
|
||||
happyLDAPCustomSessionData := &psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstreamResourceUID,
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: ldapUpstreamType,
|
||||
LDAP: &psession.LDAPSessionData{
|
||||
UserDN: ldapUpstreamDN,
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||
authcodeExchange authcodeExchangeInputs
|
||||
refreshRequest refreshRequestInputs
|
||||
name string
|
||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||
authcodeExchange authcodeExchangeInputs
|
||||
refreshRequest refreshRequestInputs
|
||||
modifyRefreshTokenStorage func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string)
|
||||
}{
|
||||
{
|
||||
name: "happy path refresh grant with openid scope granted (id token returned)",
|
||||
@ -1015,7 +1080,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||
wantRequestedScopes: []string{"offline_access"},
|
||||
wantGrantedScopes: []string{"offline_access"},
|
||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
||||
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
||||
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||
},
|
||||
@ -1096,7 +1161,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
|
||||
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
|
||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
||||
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
||||
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
|
||||
},
|
||||
@ -1400,7 +1465,7 @@ func TestRefreshGrant(t *testing.T) {
|
||||
wantErrorResponseBody: here.Doc(`
|
||||
{
|
||||
"error": "error",
|
||||
"error_description": "Error during upstream refresh. Provider 'this-name-will-not-be-found' of type 'oidc' from upstream session data was not found."
|
||||
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
|
||||
}
|
||||
`),
|
||||
},
|
||||
@ -1449,12 +1514,12 @@ func TestRefreshGrant(t *testing.T) {
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantErrorResponseBody: here.Doc(`
|
||||
{
|
||||
"error": "error",
|
||||
"error_description": "Error during upstream refresh. Upstream refresh failed using provider 'some-oidc-idp' of type 'oidc'."
|
||||
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||
}
|
||||
`),
|
||||
},
|
||||
@ -1474,13 +1539,520 @@ func TestRefreshGrant(t *testing.T) {
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(),
|
||||
wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
|
||||
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantErrorResponseBody: here.Doc(`
|
||||
{
|
||||
"error": "error",
|
||||
"error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token using provider 'some-oidc-idp' of type 'oidc'."
|
||||
"error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token."
|
||||
}
|
||||
`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upstream ldap refresh happy path",
|
||||
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,
|
||||
),
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: happyRefreshTokenResponseForLDAP(
|
||||
happyLDAPCustomSessionData,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upstream active directory refresh happy path",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: activeDirectoryUpstreamName,
|
||||
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
happyActiveDirectoryCustomSessionData,
|
||||
),
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: happyRefreshTokenResponseForActiveDirectory(
|
||||
happyActiveDirectoryCustomSessionData,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upstream ldap refresh when the LDAP session data is nil",
|
||||
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: &psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstreamResourceUID,
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: ldapUpstreamType,
|
||||
LDAP: nil,
|
||||
},
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
&psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstreamResourceUID,
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: ldapUpstreamType,
|
||||
LDAP: nil,
|
||||
},
|
||||
),
|
||||
},
|
||||
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: "upstream active directory refresh when the ad session data is nil",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: activeDirectoryUpstreamName,
|
||||
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
customSessionData: &psession.CustomSessionData{
|
||||
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||
ProviderName: activeDirectoryUpstreamName,
|
||||
ProviderType: activeDirectoryUpstreamType,
|
||||
ActiveDirectory: nil,
|
||||
},
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
&psession.CustomSessionData{
|
||||
ProviderUID: activeDirectoryUpstreamResourceUID,
|
||||
ProviderName: activeDirectoryUpstreamName,
|
||||
ProviderType: activeDirectoryUpstreamType,
|
||||
ActiveDirectory: nil,
|
||||
},
|
||||
),
|
||||
},
|
||||
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: "upstream ldap refresh when the LDAP session data does not contain dn",
|
||||
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: &psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstreamResourceUID,
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: ldapUpstreamType,
|
||||
LDAP: &psession.LDAPSessionData{
|
||||
UserDN: "",
|
||||
},
|
||||
},
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
&psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstreamResourceUID,
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: ldapUpstreamType,
|
||||
LDAP: &psession.LDAPSessionData{
|
||||
UserDN: "",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
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: "upstream active directory refresh when the active directory session data does not contain dn",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: activeDirectoryUpstreamName,
|
||||
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
customSessionData: &psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstreamResourceUID,
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: ldapUpstreamType,
|
||||
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||
UserDN: "",
|
||||
},
|
||||
},
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
&psession.CustomSessionData{
|
||||
ProviderUID: ldapUpstreamResourceUID,
|
||||
ProviderName: ldapUpstreamName,
|
||||
ProviderType: ldapUpstreamType,
|
||||
ActiveDirectory: &psession.ActiveDirectorySessionData{
|
||||
UserDN: "",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
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: "upstream ldap refresh returns an error",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: ldapUpstreamName,
|
||||
ResourceUID: ldapUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
PerformRefreshErr: errors.New("Some error performing upstream refresh"),
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
customSessionData: happyLDAPCustomSessionData,
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
happyLDAPCustomSessionData,
|
||||
),
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantErrorResponseBody: here.Doc(`
|
||||
{
|
||||
"error": "error",
|
||||
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||
}
|
||||
`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upstream active directory refresh returns an error",
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
|
||||
Name: activeDirectoryUpstreamName,
|
||||
ResourceUID: activeDirectoryUpstreamResourceUID,
|
||||
URL: ldapUpstreamURL,
|
||||
PerformRefreshErr: errors.New("Some error performing upstream refresh"),
|
||||
}),
|
||||
authcodeExchange: authcodeExchangeInputs{
|
||||
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
|
||||
customSessionData: happyActiveDirectoryCustomSessionData,
|
||||
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
|
||||
happyActiveDirectoryCustomSessionData,
|
||||
),
|
||||
},
|
||||
refreshRequest: refreshRequestInputs{
|
||||
want: tokenEndpointResponseExpectedValues{
|
||||
wantUpstreamRefreshCall: happyActiveDirectoryUpstreamRefreshCall(),
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantErrorResponseBody: here.Doc(`
|
||||
{
|
||||
"error": "error",
|
||||
"error_description": "Error during upstream refresh. Upstream refresh failed."
|
||||
}
|
||||
`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
//fositeSessionData: &openid.DefaultSession{},
|
||||
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: "username in extra is not a string",
|
||||
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,
|
||||
//fositeSessionData: &openid.DefaultSession{},
|
||||
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{}{"username": 123},
|
||||
},
|
||||
}
|
||||
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 in extra is an empty string",
|
||||
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,
|
||||
//fositeSessionData: &openid.DefaultSession{},
|
||||
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{}{"username": ""},
|
||||
},
|
||||
}
|
||||
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."
|
||||
}
|
||||
`),
|
||||
},
|
||||
@ -1493,6 +2065,8 @@ func TestRefreshGrant(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// First exchange the authcode for tokens, including a refresh token.
|
||||
// its actually fine to use this function even when simulating ldap (which uses a different flow) because it's
|
||||
// just populating a secret in storage.
|
||||
subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange, test.idps.Build())
|
||||
var parsedAuthcodeExchangeResponseBody map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody))
|
||||
@ -1511,6 +2085,10 @@ func TestRefreshGrant(t *testing.T) {
|
||||
// Send the refresh token back and preform a refresh.
|
||||
firstRefreshToken := parsedAuthcodeExchangeResponseBody["refresh_token"].(string)
|
||||
require.NotEmpty(t, firstRefreshToken)
|
||||
|
||||
if test.modifyRefreshTokenStorage != nil {
|
||||
test.modifyRefreshTokenStorage(t, oauthStore, firstRefreshToken)
|
||||
}
|
||||
reqContext := context.WithValue(context.Background(), struct{ name string }{name: "test"}, "request-context")
|
||||
req := httptest.NewRequest("POST", "/path/shouldn't/matter",
|
||||
happyRefreshRequestBody(firstRefreshToken).ReadCloser()).WithContext(reqContext)
|
||||
@ -1525,11 +2103,11 @@ func TestRefreshGrant(t *testing.T) {
|
||||
t.Logf("second response body: %q", refreshResponse.Body.String())
|
||||
|
||||
// Test that we did or did not make a call to the upstream OIDC provider interface to perform a token refresh.
|
||||
if test.refreshRequest.want.wantUpstreamOIDCRefreshCall != nil {
|
||||
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args.Ctx = reqContext
|
||||
if test.refreshRequest.want.wantUpstreamRefreshCall != nil {
|
||||
test.refreshRequest.want.wantUpstreamRefreshCall.args.Ctx = reqContext
|
||||
test.idps.RequireExactlyOneCallToPerformRefresh(t,
|
||||
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.performedByUpstreamName,
|
||||
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args,
|
||||
test.refreshRequest.want.wantUpstreamRefreshCall.performedByUpstreamName,
|
||||
test.refreshRequest.want.wantUpstreamRefreshCall.args,
|
||||
)
|
||||
} else {
|
||||
test.idps.RequireExactlyZeroCallsToPerformRefresh(t)
|
||||
|
@ -45,6 +45,10 @@ type CustomSessionData struct {
|
||||
|
||||
// Only used when ProviderType == "oidc".
|
||||
OIDC *OIDCSessionData `json:"oidc,omitempty"`
|
||||
|
||||
LDAP *LDAPSessionData `json:"ldap,omitempty"`
|
||||
|
||||
ActiveDirectory *ActiveDirectorySessionData `json:"activedirectory,omitempty"`
|
||||
}
|
||||
|
||||
type ProviderType string
|
||||
@ -60,6 +64,16 @@ type OIDCSessionData struct {
|
||||
UpstreamRefreshToken string `json:"upstreamRefreshToken"`
|
||||
}
|
||||
|
||||
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
|
||||
type LDAPSessionData struct {
|
||||
UserDN string `json:"userDN"`
|
||||
}
|
||||
|
||||
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
|
||||
type ActiveDirectorySessionData struct {
|
||||
UserDN string `json:"userDN"`
|
||||
}
|
||||
|
||||
// NewPinnipedSession returns a new empty session.
|
||||
func NewPinnipedSession() *PinnipedSession {
|
||||
return &PinnipedSession{
|
||||
|
@ -21,10 +21,10 @@ import (
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
|
||||
"go.pinniped.dev/internal/authenticators"
|
||||
"go.pinniped.dev/internal/crud"
|
||||
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
||||
"go.pinniped.dev/internal/fositestorage/openidconnect"
|
||||
@ -61,8 +61,11 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct {
|
||||
// PerformRefreshArgs is used to spy on calls to
|
||||
// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc().
|
||||
type PerformRefreshArgs struct {
|
||||
Ctx context.Context
|
||||
RefreshToken string
|
||||
Ctx context.Context
|
||||
RefreshToken string
|
||||
DN string
|
||||
ExpectedUsername string
|
||||
ExpectedSubject string
|
||||
}
|
||||
|
||||
// RevokeRefreshTokenArgs is used to spy on calls to
|
||||
@ -81,10 +84,13 @@ type ValidateTokenArgs struct {
|
||||
}
|
||||
|
||||
type TestUpstreamLDAPIdentityProvider struct {
|
||||
Name string
|
||||
ResourceUID types.UID
|
||||
URL *url.URL
|
||||
AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error)
|
||||
Name string
|
||||
ResourceUID types.UID
|
||||
URL *url.URL
|
||||
AuthenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error)
|
||||
performRefreshCallCount int
|
||||
performRefreshArgs []*PerformRefreshArgs
|
||||
PerformRefreshErr error
|
||||
}
|
||||
|
||||
var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{}
|
||||
@ -97,7 +103,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetName() string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||
func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||
return u.AuthenticateFunc(ctx, username, password)
|
||||
}
|
||||
|
||||
@ -105,6 +111,34 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
|
||||
return u.URL
|
||||
}
|
||||
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
||||
if u.performRefreshArgs == nil {
|
||||
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
||||
}
|
||||
u.performRefreshCallCount++
|
||||
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
|
||||
Ctx: ctx,
|
||||
DN: userDN,
|
||||
ExpectedUsername: expectedUsername,
|
||||
ExpectedSubject: expectedSubject,
|
||||
})
|
||||
if u.PerformRefreshErr != nil {
|
||||
return u.PerformRefreshErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshCallCount() int {
|
||||
return u.performRefreshCallCount
|
||||
}
|
||||
|
||||
func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs {
|
||||
if u.performRefreshArgs == nil {
|
||||
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
|
||||
}
|
||||
return u.performRefreshArgs[call]
|
||||
}
|
||||
|
||||
type TestUpstreamOIDCIdentityProvider struct {
|
||||
Name string
|
||||
ClientID string
|
||||
@ -429,31 +463,54 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh(
|
||||
t.Helper()
|
||||
var actualArgs *PerformRefreshArgs
|
||||
var actualNameOfUpstreamWhichMadeCall string
|
||||
actualCallCountAcrossAllOIDCUpstreams := 0
|
||||
actualCallCountAcrossAllUpstreams := 0
|
||||
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
||||
callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount
|
||||
actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream
|
||||
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
|
||||
if callCountOnThisUpstream == 1 {
|
||||
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
|
||||
actualArgs = upstreamOIDC.performRefreshArgs[0]
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams,
|
||||
"should have been exactly one call to PerformRefresh() by all OIDC upstreams",
|
||||
for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders {
|
||||
callCountOnThisUpstream := upstreamLDAP.performRefreshCallCount
|
||||
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
|
||||
if callCountOnThisUpstream == 1 {
|
||||
actualNameOfUpstreamWhichMadeCall = upstreamLDAP.Name
|
||||
actualArgs = upstreamLDAP.performRefreshArgs[0]
|
||||
}
|
||||
}
|
||||
for _, upstreamAD := range b.upstreamActiveDirectoryIdentityProviders {
|
||||
callCountOnThisUpstream := upstreamAD.performRefreshCallCount
|
||||
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
|
||||
if callCountOnThisUpstream == 1 {
|
||||
actualNameOfUpstreamWhichMadeCall = upstreamAD.Name
|
||||
actualArgs = upstreamAD.performRefreshArgs[0]
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, actualCallCountAcrossAllUpstreams,
|
||||
"should have been exactly one call to PerformRefresh() by all upstreams",
|
||||
)
|
||||
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
|
||||
"PerformRefresh() was called on the wrong OIDC upstream",
|
||||
"PerformRefresh() was called on the wrong upstream",
|
||||
)
|
||||
require.Equal(t, expectedArgs, actualArgs)
|
||||
}
|
||||
|
||||
func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) {
|
||||
t.Helper()
|
||||
actualCallCountAcrossAllOIDCUpstreams := 0
|
||||
actualCallCountAcrossAllUpstreams := 0
|
||||
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
|
||||
actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.performRefreshCallCount
|
||||
actualCallCountAcrossAllUpstreams += upstreamOIDC.performRefreshCallCount
|
||||
}
|
||||
require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams,
|
||||
for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders {
|
||||
actualCallCountAcrossAllUpstreams += upstreamLDAP.performRefreshCallCount
|
||||
}
|
||||
for _, upstreamActiveDirectory := range b.upstreamActiveDirectoryIdentityProviders {
|
||||
actualCallCountAcrossAllUpstreams += upstreamActiveDirectory.performRefreshCallCount
|
||||
}
|
||||
|
||||
require.Equal(t, 0, actualCallCountAcrossAllUpstreams,
|
||||
"expected exactly zero calls to PerformRefresh()",
|
||||
)
|
||||
}
|
||||
|
@ -21,12 +21,12 @@ import (
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/google/uuid"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/utils/trace"
|
||||
|
||||
"go.pinniped.dev/internal/authenticators"
|
||||
"go.pinniped.dev/internal/endpointaddr"
|
||||
"go.pinniped.dev/internal/oidc/downstreamsession"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
@ -169,6 +169,73 @@ func (p *Provider) GetConfig() ProviderConfig {
|
||||
return p.c
|
||||
}
|
||||
|
||||
func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
|
||||
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
|
||||
searchResult, err := p.performRefresh(ctx, userDN)
|
||||
if err != nil {
|
||||
p.traceRefreshFailure(t, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
if len(searchResult.Entries) != 1 {
|
||||
return fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`,
|
||||
userDN, len(searchResult.Entries),
|
||||
)
|
||||
}
|
||||
|
||||
userEntry := searchResult.Entries[0]
|
||||
if len(userEntry.DN) == 0 {
|
||||
return fmt.Errorf(`searching for user with original DN "%s" resulted in search result without DN`, userDN)
|
||||
}
|
||||
|
||||
newUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, userDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newUsername != expectedUsername {
|
||||
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
|
||||
userDN, expectedUsername, newUsername,
|
||||
)
|
||||
}
|
||||
|
||||
newUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, userDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
|
||||
if newSubject != expectedSubject {
|
||||
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, expectedSubject, newSubject)
|
||||
}
|
||||
|
||||
// we checked that the user still exists and their information is the same, so just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) performRefresh(ctx context.Context, userDN string) (*ldap.SearchResult, error) {
|
||||
search := p.refreshUserSearchRequest(userDN)
|
||||
|
||||
conn, err := p.dial(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
|
||||
}
|
||||
|
||||
searchResult, err := conn.Search(search)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`error searching for user "%s": %w`, userDN, err)
|
||||
}
|
||||
return searchResult, nil
|
||||
}
|
||||
|
||||
func (p *Provider) dial(ctx context.Context) (Conn, error) {
|
||||
tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort)
|
||||
if err != nil {
|
||||
@ -310,7 +377,7 @@ func (p *Provider) TestConnection(ctx context.Context) error {
|
||||
// authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does
|
||||
// not bind as that user, so it does not test their password. It returns the same values that a real call to
|
||||
// AuthenticateUser with the correct password would return.
|
||||
func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticator.Response, bool, error) {
|
||||
func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticators.Response, bool, error) {
|
||||
endUserBindFunc := func(conn Conn, foundUserDN string) error {
|
||||
// Act as if the end user bind always succeeds.
|
||||
return nil
|
||||
@ -319,14 +386,14 @@ func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string)
|
||||
}
|
||||
|
||||
// Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
|
||||
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
|
||||
endUserBindFunc := func(conn Conn, foundUserDN string) error {
|
||||
return conn.Bind(foundUserDN, password)
|
||||
}
|
||||
return p.authenticateUserImpl(ctx, username, endUserBindFunc)
|
||||
}
|
||||
|
||||
func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticator.Response, bool, error) {
|
||||
func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, bool, error) {
|
||||
t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()})
|
||||
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
|
||||
|
||||
@ -355,24 +422,16 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi
|
||||
return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
|
||||
}
|
||||
|
||||
mappedUsername, mappedUID, mappedGroupNames, err := p.searchAndBindUser(conn, username, bindFunc)
|
||||
response, err := p.searchAndBindUser(conn, username, bindFunc)
|
||||
if err != nil {
|
||||
p.traceAuthFailure(t, err)
|
||||
return nil, false, err
|
||||
}
|
||||
if len(mappedUsername) == 0 || len(mappedUID) == 0 {
|
||||
// Couldn't find the username or couldn't bind using the password.
|
||||
if response == nil {
|
||||
p.traceAuthFailure(t, fmt.Errorf("bad username or password"))
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
response := &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: mappedUsername,
|
||||
UID: mappedUID,
|
||||
Groups: mappedGroupNames,
|
||||
},
|
||||
}
|
||||
p.traceAuthSuccess(t)
|
||||
return response, true, nil
|
||||
}
|
||||
@ -454,7 +513,7 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e
|
||||
return searchBase, nil
|
||||
}
|
||||
|
||||
func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) {
|
||||
func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, error) {
|
||||
searchResult, err := conn.Search(p.userSearchRequest(username))
|
||||
if err != nil {
|
||||
plog.All(`error searching for user`,
|
||||
@ -462,7 +521,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
||||
"username", username,
|
||||
"err", err,
|
||||
)
|
||||
return "", "", nil, fmt.Errorf(`error searching for user: %w`, err)
|
||||
return nil, fmt.Errorf(`error searching for user: %w`, err)
|
||||
}
|
||||
if len(searchResult.Entries) == 0 {
|
||||
if plog.Enabled(plog.LevelAll) {
|
||||
@ -473,38 +532,38 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
||||
} else {
|
||||
plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName())
|
||||
}
|
||||
return "", "", nil, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 "%s" resulted in %d search results, but expected 1 result`,
|
||||
username, len(searchResult.Entries),
|
||||
)
|
||||
}
|
||||
userEntry := searchResult.Entries[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 "%s" resulted in search result without DN`, username)
|
||||
}
|
||||
|
||||
mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We would like to support binary typed attributes for UIDs, so always read them as binary and encode them,
|
||||
// even when the attribute may not be binary.
|
||||
mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, username)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mappedGroupNames := []string{}
|
||||
if len(p.c.GroupSearch.Base) > 0 {
|
||||
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
sort.Strings(mappedGroupNames)
|
||||
@ -516,12 +575,26 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
|
||||
err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN)
|
||||
ldapErr := &ldap.Error{}
|
||||
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 "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err)
|
||||
}
|
||||
|
||||
return mappedUsername, mappedUID, mappedGroupNames, nil
|
||||
if len(mappedUsername) == 0 || len(mappedUID) == 0 {
|
||||
// Couldn't find the username or couldn't bind using the password.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
response := &authenticators.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: mappedUsername,
|
||||
UID: mappedUID,
|
||||
Groups: mappedGroupNames,
|
||||
},
|
||||
DN: userEntry.DN,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *Provider) defaultNamingContextRequest() *ldap.SearchRequest {
|
||||
@ -568,6 +641,21 @@ func (p *Provider) groupSearchRequest(userDN string) *ldap.SearchRequest {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) refreshUserSearchRequest(dn string) *ldap.SearchRequest {
|
||||
// See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options.
|
||||
return &ldap.SearchRequest{
|
||||
BaseDN: dn,
|
||||
Scope: ldap.ScopeBaseObject,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
SizeLimit: 2,
|
||||
TimeLimit: 90,
|
||||
TypesOnly: false,
|
||||
Filter: "(objectClass=*)", // we already have the dn, so the filter doesn't matter
|
||||
Attributes: p.userSearchRequestedAttributes(),
|
||||
Controls: nil, // this could be used to enable paging, but we're already limiting the result max size
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) userSearchRequestedAttributes() []string {
|
||||
attributes := []string{}
|
||||
if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName {
|
||||
@ -687,6 +775,12 @@ func (p *Provider) traceSearchBaseDiscoveryFailure(t *trace.Trace, err error) {
|
||||
trace.Field{Key: "reason", Value: err.Error()})
|
||||
}
|
||||
|
||||
func (p *Provider) traceRefreshFailure(t *trace.Trace, err error) {
|
||||
t.Step("refresh failed",
|
||||
trace.Field{Key: "reason", Value: err.Error()},
|
||||
)
|
||||
}
|
||||
|
||||
func MicrosoftUUIDFromBinary(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) {
|
||||
|
@ -18,9 +18,9 @@ import (
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
|
||||
"go.pinniped.dev/internal/authenticators"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/endpointaddr"
|
||||
"go.pinniped.dev/internal/mocks/mockldapconn"
|
||||
@ -151,7 +151,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
}
|
||||
|
||||
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
|
||||
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticator.Response {
|
||||
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticators.Response {
|
||||
u := &user.DefaultInfo{
|
||||
Name: testUserSearchResultUsernameAttributeValue,
|
||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||
@ -160,7 +160,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
if editFunc != nil {
|
||||
editFunc(u)
|
||||
}
|
||||
return &authenticator.Response{User: u}
|
||||
return &authenticators.Response{User: u, DN: testUserSearchResultDNValue}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@ -173,7 +173,7 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
dialError error
|
||||
wantError string
|
||||
wantToSkipDial bool
|
||||
wantAuthResponse *authenticator.Response
|
||||
wantAuthResponse *authenticators.Response
|
||||
wantUnauthenticated bool
|
||||
skipDryRunAuthenticateUser bool // tests about when the end user bind fails don't make sense for DryRunAuthenticateUser()
|
||||
}{
|
||||
@ -498,12 +498,13 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
bindEndUserMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
|
||||
},
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: testUserSearchResultUsernameAttributeValue,
|
||||
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
|
||||
Groups: []string{"a", "b", "c"},
|
||||
},
|
||||
DN: testUserSearchResultDNValue,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -1212,6 +1213,340 @@ func TestEndUserAuthentication(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamRefresh(t *testing.T) {
|
||||
expectedUserSearch := &ldap.SearchRequest{
|
||||
BaseDN: testUserSearchResultDNValue,
|
||||
Scope: ldap.ScopeBaseObject,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
SizeLimit: 2,
|
||||
TimeLimit: 90,
|
||||
TypesOnly: false,
|
||||
Filter: "(objectClass=*)",
|
||||
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute},
|
||||
Controls: nil, // don't need paging because we set the SizeLimit so small
|
||||
}
|
||||
|
||||
happyPathUserSearchResult := &ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{
|
||||
{
|
||||
DN: testUserSearchResultDNValue,
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: testUserSearchUsernameAttribute,
|
||||
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||
},
|
||||
{
|
||||
Name: testUserSearchUIDAttribute,
|
||||
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
providerConfig := &ProviderConfig{
|
||||
Name: "some-provider-name",
|
||||
Host: testHost,
|
||||
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
|
||||
ConnectionProtocol: TLS,
|
||||
BindUsername: testBindUsername,
|
||||
BindPassword: testBindPassword,
|
||||
UserSearch: UserSearchConfig{
|
||||
Base: testUserSearchBase,
|
||||
UIDAttribute: testUserSearchUIDAttribute,
|
||||
UsernameAttribute: testUserSearchUsernameAttribute,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
providerConfig *ProviderConfig
|
||||
setupMocks func(conn *mockldapconn.MockConn)
|
||||
dialError error
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "happy path where searching the dn returns a single entry",
|
||||
providerConfig: providerConfig,
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error where dial fails",
|
||||
providerConfig: providerConfig,
|
||||
dialError: errors.New("some dial error"),
|
||||
wantErr: "error dialing host \"ldap.example.com:8443\": some dial error",
|
||||
},
|
||||
{
|
||||
name: "error binding",
|
||||
providerConfig: providerConfig,
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "error binding as \"cn=some-bind-username,dc=pinniped,dc=dev\" before user search: some bind error",
|
||||
},
|
||||
{
|
||||
name: "search result returns no entries",
|
||||
providerConfig: providerConfig,
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "searching for user \"some-upstream-user-dn\" resulted in 0 search results, but expected 1 result",
|
||||
},
|
||||
{
|
||||
name: "error searching",
|
||||
providerConfig: providerConfig,
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(nil, errors.New("some search error"))
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "error searching for user \"some-upstream-user-dn\": some search error",
|
||||
},
|
||||
{
|
||||
name: "search result returns more than one entry",
|
||||
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{},
|
||||
},
|
||||
{
|
||||
DN: "doesn't-matter",
|
||||
Attributes: []*ldap.EntryAttribute{},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "searching for user \"some-upstream-user-dn\" resulted in 2 search results, but expected 1 result",
|
||||
},
|
||||
{
|
||||
name: "search result has wrong uid",
|
||||
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("wrong-uid")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=d3JvbmctdWlk\"",
|
||||
},
|
||||
{
|
||||
name: "search result has wrong username",
|
||||
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{"wrong-username"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "searching for user \"some-upstream-user-dn\" returned a different username than the previous value. expected: \"some-upstream-username-value\", actual: \"wrong-username\"",
|
||||
},
|
||||
{
|
||||
name: "search result has no dn",
|
||||
providerConfig: providerConfig,
|
||||
setupMocks: func(conn *mockldapconn.MockConn) {
|
||||
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
|
||||
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
|
||||
Entries: []*ldap.Entry{
|
||||
{
|
||||
Attributes: []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: testUserSearchUsernameAttribute,
|
||||
Values: []string{testUserSearchResultUsernameAttributeValue},
|
||||
},
|
||||
{
|
||||
Name: testUserSearchUIDAttribute,
|
||||
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "searching for user with original DN \"some-upstream-user-dn\" resulted in search result without DN",
|
||||
},
|
||||
{
|
||||
name: "search result has 0 values for username attribute",
|
||||
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{},
|
||||
},
|
||||
{
|
||||
Name: testUserSearchUIDAttribute,
|
||||
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "found 0 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||
},
|
||||
{
|
||||
name: "search result has more than one value for username attribute",
|
||||
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, "something-else"},
|
||||
},
|
||||
{
|
||||
Name: testUserSearchUIDAttribute,
|
||||
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "found 2 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||
},
|
||||
{
|
||||
name: "search result has 0 values for uid attribute",
|
||||
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{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "found 0 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||
},
|
||||
{
|
||||
name: "search result has 2 values for uid attribute",
|
||||
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), []byte("other-uid-value")},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Times(1)
|
||||
conn.EXPECT().Close().Times(1)
|
||||
},
|
||||
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
t.Cleanup(ctrl.Finish)
|
||||
|
||||
conn := mockldapconn.NewMockConn(ctrl)
|
||||
if tt.setupMocks != nil {
|
||||
tt.setupMocks(conn)
|
||||
}
|
||||
|
||||
dialWasAttempted := false
|
||||
providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) {
|
||||
dialWasAttempted = true
|
||||
require.Equal(t, providerConfig.Host, addr.Endpoint())
|
||||
if tt.dialError != nil {
|
||||
return nil, tt.dialError
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
})
|
||||
|
||||
provider := New(*providerConfig)
|
||||
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
|
||||
err := provider.PerformRefresh(context.Background(), testUserSearchResultDNValue, testUserSearchResultUsernameAttributeValue, subject)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, tt.wantErr, err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.Equal(t, true, dialWasAttempted)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestConnection(t *testing.T) {
|
||||
providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig {
|
||||
config := &ProviderConfig{
|
||||
|
@ -13,6 +13,10 @@ As a Kubernetes cluster administrator or user, you can learn how Pinniped works,
|
||||
|
||||
Have a question, comment, or idea? Please reach out via [GitHub Discussions](https://github.com/vmware-tanzu/pinniped/discussions) or [join the Pinniped community meetings]({{< ref "/community" >}}).
|
||||
|
||||
## Background
|
||||
|
||||
{{< docsmenu "background" >}}
|
||||
|
||||
## Tutorials
|
||||
|
||||
{{< docsmenu "tutorials" >}}
|
||||
@ -24,7 +28,3 @@ Have a question, comment, or idea? Please reach out via [GitHub Discussions](htt
|
||||
## Reference
|
||||
|
||||
{{< docsmenu "reference" >}}
|
||||
|
||||
## Background
|
||||
|
||||
{{< docsmenu "background" >}}
|
||||
|
@ -6,7 +6,7 @@ menu:
|
||||
docs:
|
||||
name: Background
|
||||
identifier: background
|
||||
weight: 110
|
||||
weight: 11
|
||||
---
|
||||
|
||||
{{< docsmenu "background" >}}
|
||||
|
@ -100,7 +100,7 @@ issue short-lived cluster certificates. (In the future, when the Kubernetes CSR
|
||||
provides a way to issue short-lived certificates, then the Pinniped credential exchange API
|
||||
will use that instead of using the cluster's signing keypair.)
|
||||
* Impersonation Proxy: Pinniped hosts an [impersonation](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation)
|
||||
proxy that sends requests to the Kubernetes API server with user information and permissions based on a token.
|
||||
proxy that sends requests to the Kubernetes API server with user information and permissions based on a token.
|
||||
|
||||
## kubectl Integration
|
||||
|
||||
|
@ -4,6 +4,12 @@
|
||||
status = 302
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/cli"
|
||||
to = "https://pinniped.dev/docs/howto/install-cli"
|
||||
status = 302
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/latest/*"
|
||||
to = "https://github.com/vmware-tanzu/pinniped/releases/latest/download/:splat"
|
||||
|
@ -17,9 +17,9 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
|
||||
"go.pinniped.dev/internal/authenticators"
|
||||
"go.pinniped.dev/internal/upstreamldap"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
@ -74,7 +74,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
password string
|
||||
provider *upstreamldap.Provider
|
||||
wantError string
|
||||
wantAuthResponse *authenticator.Response
|
||||
wantAuthResponse *authenticators.Response
|
||||
wantUnauthenticated bool
|
||||
}{
|
||||
{
|
||||
@ -82,8 +82,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(nil)),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -94,8 +94,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -103,8 +103,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -112,8 +112,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -124,8 +124,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.UserSearch.UsernameAttribute = "dn"
|
||||
p.UserSearch.Filter = "cn={}"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -135,8 +135,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -146,8 +146,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -155,8 +155,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -164,8 +164,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -173,8 +173,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
username: "seAl", // note that this is not case-sensitive! sn=Seal. The server decides which fields are compared case-sensitive.
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, // note that the final answer has case preserved from the entry
|
||||
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
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -186,8 +186,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.UserSearch.UsernameAttribute = "givenName"
|
||||
p.UserSearch.UIDAttribute = "givenName"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -198,8 +198,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
p.UserSearch.Filter = "givenName={}"
|
||||
p.UserSearch.UsernameAttribute = "cn"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -219,8 +219,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Base = ""
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -230,8 +230,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -241,11 +241,11 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.GroupNameAttribute = "dn"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||
}},
|
||||
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -255,11 +255,11 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.GroupNameAttribute = ""
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
|
||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||
}},
|
||||
}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -269,8 +269,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}},
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -280,8 +280,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}},
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -291,8 +291,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
|
||||
wantAuthResponse: &authenticators.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -670,14 +670,15 @@ func TestSimultaneousLDAPRequestsOnSingleProvider(t *testing.T) {
|
||||
// Record failures but allow the test to keep running so that all the background goroutines have a chance to try.
|
||||
assert.NoError(t, result.err)
|
||||
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
|
||||
assert.Equal(t, &authenticator.Response{
|
||||
assert.Equal(t, &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",
|
||||
}, result.response)
|
||||
}
|
||||
}
|
||||
|
||||
type authUserResult struct {
|
||||
response *authenticator.Response
|
||||
response *authenticators.Response
|
||||
authenticated bool
|
||||
err error
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
maybeSkip func(t *testing.T)
|
||||
createIDP func(t *testing.T)
|
||||
createIDP func(t *testing.T) string
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
wantDownstreamIDTokenUsernameToMatch string
|
||||
@ -55,16 +55,16 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
// We don't necessarily have any way to revoke the user's session on the upstream provider,
|
||||
// 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.
|
||||
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData)
|
||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName string)
|
||||
}{
|
||||
{
|
||||
name: "oidc with default username and groups claim settings",
|
||||
maybeSkip: func(t *testing.T) {
|
||||
// never need to skip this test
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
@ -73,9 +73,11 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
return oidcIDP.Name
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||
@ -90,9 +92,9 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
maybeSkip: func(t *testing.T) {
|
||||
// never need to skip this test
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
@ -108,9 +110,11 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
return oidcIDP.Name
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
|
||||
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||
@ -124,9 +128,9 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
maybeSkip: func(t *testing.T) {
|
||||
// never need to skip this test
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
@ -138,6 +142,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
AllowPasswordGrant: true, // allow the CLI password flow for this OIDCIdentityProvider
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
return oidcIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -148,7 +153,8 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) {
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
|
||||
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
|
||||
@ -166,7 +172,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
@ -204,6 +210,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -214,7 +221,13 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||
fositeSessionData := pinnipedSession.Fosite
|
||||
fositeSessionData.Claims.Subject = "not-right"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||
@ -233,7 +246,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
@ -271,6 +284,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -281,7 +295,13 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||
fositeSessionData := pinnipedSession.Fosite
|
||||
fositeSessionData.Claims.Extra["username"] = "not-the-same"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+
|
||||
@ -300,7 +320,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
@ -338,6 +358,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -348,9 +369,8 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
true,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
|
||||
wantErrorType: "access_denied",
|
||||
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
|
||||
wantErrorType: "access_denied",
|
||||
},
|
||||
{
|
||||
name: "ldap login still works after updating bind secret",
|
||||
@ -360,7 +380,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
@ -416,6 +436,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
requireEventually.NoError(err)
|
||||
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
|
||||
}, time.Minute, 500*time.Millisecond)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -426,7 +447,12 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||
customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||
@ -445,7 +471,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("LDAP integration test requires connectivity to an LDAP server")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
@ -515,6 +541,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
requireEventually.NoError(err)
|
||||
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
|
||||
}, time.Minute, 500*time.Millisecond)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -525,7 +552,12 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.LDAP.UserDN)
|
||||
customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||
@ -547,7 +579,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("Active Directory hostname not specified")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
@ -570,6 +602,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||
return adIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -580,7 +613,13 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||
fositeSessionData := pinnipedSession.Fosite
|
||||
fositeSessionData.Claims.Extra["username"] = "not-the-same"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||
@ -601,7 +640,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("Active Directory hostname not specified")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
@ -638,6 +677,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||
return adIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -648,7 +688,13 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||
fositeSessionData := pinnipedSession.Fosite
|
||||
fositeSessionData.Claims.Subject = "not-right"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||
@ -670,7 +716,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("Active Directory hostname not specified")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||
@ -711,6 +757,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
requireEventually.NoError(err)
|
||||
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
|
||||
}, time.Minute, 500*time.Millisecond)
|
||||
return adIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -721,7 +768,12 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||
customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||
@ -743,7 +795,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("Active Directory hostname not specified")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||
@ -799,6 +851,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
requireEventually.NoError(err)
|
||||
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
|
||||
}, time.Minute, 500*time.Millisecond)
|
||||
return adIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -809,7 +862,12 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
|
||||
customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
|
||||
@ -831,7 +889,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
t.Skip("Active Directory hostname not specified")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) {
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
@ -854,6 +912,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
|
||||
return adIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
@ -864,10 +923,93 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
true,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type
|
||||
breakRefreshSessionData: nil,
|
||||
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
|
||||
wantErrorType: "access_denied",
|
||||
},
|
||||
{
|
||||
name: "ldap refresh fails when username changes from email as username to dn as username",
|
||||
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")
|
||||
}
|
||||
},
|
||||
createIDP: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername,
|
||||
v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
|
||||
},
|
||||
)
|
||||
ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamLDAP.Host,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
|
||||
},
|
||||
Bind: idpv1alpha1.LDAPIdentityProviderBind{
|
||||
SecretName: secret.Name,
|
||||
},
|
||||
UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.UserSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{
|
||||
Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName,
|
||||
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.GroupSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
|
||||
GroupName: "dn",
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
expectedMsg := fmt.Sprintf(
|
||||
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||
env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername,
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
return ldapIDP.Name
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login
|
||||
httpClient,
|
||||
false,
|
||||
)
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string) {
|
||||
// get the idp, update the config.
|
||||
client := testlib.NewSupervisorClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create the LDAPIdentityProvider using GenerateName to get a random name.
|
||||
upstreams := client.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace)
|
||||
ldapIDP, err := upstreams.Get(ctx, idpName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
ldapIDP.Spec.UserSearch.Attributes.Username = "dn"
|
||||
|
||||
_, err = upstreams.Update(ctx, ldapIDP, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
time.Sleep(10 * time.Second) // wait for controllers to pick up the change
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
|
||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
@ -1007,9 +1149,9 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes
|
||||
|
||||
func testSupervisorLogin(
|
||||
t *testing.T,
|
||||
createIDP func(t *testing.T),
|
||||
createIDP func(t *testing.T) string,
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
|
||||
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData),
|
||||
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string),
|
||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
||||
wantErrorDescription string, wantErrorType string,
|
||||
) {
|
||||
@ -1097,7 +1239,7 @@ func testSupervisorLogin(
|
||||
}, 30*time.Second, 200*time.Millisecond)
|
||||
|
||||
// Create upstream IDP and wait for it to become ready.
|
||||
createIDP(t)
|
||||
idpName := createIDP(t)
|
||||
|
||||
// Perform OIDC discovery for our downstream.
|
||||
var discovery *coreosoidc.Provider
|
||||
@ -1191,7 +1333,7 @@ func testSupervisorLogin(
|
||||
// Next mutate the part of the session that is used during upstream refresh.
|
||||
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
|
||||
require.True(t, ok, "should have been able to cast session data to PinnipedSession")
|
||||
breakRefreshSessionData(t, pinnipedSession.Custom)
|
||||
breakRefreshSessionData(t, pinnipedSession, idpName)
|
||||
|
||||
// Then save the mutated Secret back to Kubernetes.
|
||||
// There is no update function, so delete and create again at the same name.
|
||||
@ -1204,9 +1346,8 @@ func testSupervisorLogin(
|
||||
require.Error(t, err)
|
||||
require.Regexp(t,
|
||||
regexp.QuoteMeta("oauth2: cannot fetch token: 401 Unauthorized\n")+
|
||||
regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed using provider '`)+
|
||||
"[^']+"+ // this would be the name of the identity provider CR
|
||||
regexp.QuoteMeta(fmt.Sprintf(`' of type '%s'."`, pinnipedSession.Custom.ProviderType)),
|
||||
regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed`)+
|
||||
"[^']+",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
@ -40,6 +40,12 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
|
||||
Message: `failed to perform OIDC discovery against "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee":
|
||||
Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration": dial tcp: address 444444: in [truncated 10 chars]`,
|
||||
},
|
||||
{
|
||||
Type: "AdditionalAuthorizeParametersValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "additionalAuthorizeParameters parameter names are allowed",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -72,6 +78,12 @@ Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nananananan
|
||||
Message: `failed to perform OIDC discovery against "` + env.SupervisorUpstreamOIDC.Issuer + `/":
|
||||
oidc: issuer did not match the issuer returned by provider, expected "` + env.SupervisorUpstreamOIDC.Issuer + `/" got "` + env.SupervisorUpstreamOIDC.Issuer + `"`,
|
||||
},
|
||||
{
|
||||
Type: "AdditionalAuthorizeParametersValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "additionalAuthorizeParameters parameter names are allowed",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -103,6 +115,12 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su
|
||||
Reason: "Success",
|
||||
Message: "discovered issuer configuration",
|
||||
},
|
||||
{
|
||||
Type: "AdditionalAuthorizeParametersValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "additionalAuthorizeParameters parameter names are allowed",
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user